Chapter 8 of 24 · Python for Beginners — Complete Free Tutorial
Lists and Tuples
Chapter 7 walked over data one item at a time; this chapter of the Python for Beginners series is about the things you walk over. Lists are Python's growable, ordered, changeable collection — the workhorse of almost every script. Tuples look similar but refuse to change, which makes them ideal for fixed records. You will create both, index and slice them, learn the methods that mutate a list, see the classic “two names, one list” trap, and finish with a worked example that unpacks tuples inside a loop.
Creating lists, indexing and slicing
A list is written with square brackets and commas. It can hold anything, including a mixture of types, and keeps the order you wrote it in. Positions count from 0, negative numbers count back from the end with -1 as the last item, and a slice start:stop stops before the stop position:
basket = ["apple", "banana", "cherry", "date", "elderberry"]
mixed = [1, "two", 3.0, True]
print(basket[0], basket[-1])
print(basket[1:3], basket[:2], basket[3:])
print(basket[::2], basket[::-1])
print(mixed, list("python"))
Expected output:
apple elderberry
['banana', 'cherry'] ['apple', 'banana'] ['date', 'elderberry']
['apple', 'cherry', 'elderberry'] ['elderberry', 'date', 'cherry', 'banana', 'apple']
[1, 'two', 3.0, True] ['p', 'y', 't', 'h', 'o', 'n']
Three construction habits are worth forming: list(iterable) converts a string, a range or any sequence into a fresh list; a literal you never intend to change is still written with brackets; and positions behave exactly like arrays in the JavaScript series. Notice the second output line: slicing returns a new list and never edits the original, while the third slice position is the step — [::2] takes every second item and [::-1] reverses. Out-of-range indexes raise IndexError rather than returning undefined, so a wrong position is loud instead of silent.
The methods that change a list
Lists are mutable: they carry methods that edit themselves in place. Nearly all of them return None, because the list itself is the result.
| Method | What it does | Returns |
|---|---|---|
append(item) | Adds one item to the end | None |
extend(iterable) | Splices in every item of another sequence | None |
insert(index, item) | Inserts at a position, shifting later items | None |
remove(item) | Deletes the first matching value (ValueError if absent) | None |
pop(index) | Removes by position — or the last item — and hands it back | the removed item |
reverse() | Reverses the list in place | None |
sort() | Sorts the list in place | None |
Sorting comes in two flavours: sort() edits the list where it stands and answers None, while the built-in sorted(list) leaves the original alone and returns a new list — so tasks = tasks.sort() is the classic way to turn a list into None. Run every method in the table on one practice list:
tasks = ["write tests", "fix bug", "ship release"]
tasks.append("review PR")
tasks.insert(0, "deploy hotfix")
tasks.remove("fix bug")
print(tasks)
print(tasks.pop(), tasks)
nums = [5, 2, 9, 1]
nums.sort()
print(nums, sorted(nums, reverse=True))
nums.reverse()
print(nums)
Expected output:
['deploy hotfix', 'write tests', 'ship release', 'review PR']
review PR ['deploy hotfix', 'write tests', 'ship release']
[1, 2, 5, 9] [9, 5, 2, 1]
[9, 5, 2, 1]
Two names, one list: aliasing and copying
Assigning a list does not copy it. Both names point at the same object in memory, so an edit through one is visible through the other — this is where many beginner bugs are born. is asks whether two names hold the same object, and to get an independent list you must copy explicitly: copy(), slicing with [:] and list(x) all do the same shallow job.
original = [1, 2, 3]
alias = original
alias.append(4)
print(original, alias, original is alias)
base = [1, 2, 3]
copy1 = base.copy()
copy2 = base[:]
copy1.append(99)
print(base, copy1, copy2)
Expected output:
[1, 2, 3, 4] [1, 2, 3, 4] True
[1, 2, 3] [1, 2, 3, 99] [1, 2, 3]
“Shallow” matters once your list contains other lists: the inner objects are still shared, so nested = [[1, 2], [3, 4]] copied with shallow = nested.copy() and then edited with shallow[0].append(99) changes the original too. When every level must be independent, reach for copy.deepcopy(), which rebuilds the whole tree. Deep copies are the heavy hammer: they cost time and hide the aliasing problem rather than remove it, so prefer returning new values over copying structures you did not create.
Tuples: records that cannot change
A tuple is written with commas (the parentheses are just grouping) and has no mutating methods. Try to overwrite an element and the interpreter refuses:
point = (3, 7)
x, y = point
print(x, y)
point[0] = 5
Expected output: two numbers, then a traceback ending with
3 7
TypeError: 'tuple' object does not support item assignment
Immutability is a feature, and three situations call for it:
- Keys of a dictionary — keys must be hashable, so a tuple works and a list is rejected with
TypeError: unhashable type: 'list'. - Returning several values —
return min(numbers), max(numbers)hands back one tuple that the caller unpacks in a single assignment. - Fixed records — coordinates, dates, rows of data. Nothing can accidentally reorder or resize them, and they are safe to share or use as set elements.
locations = {(40.7, -74.0): "New York", (51.5, -0.1): "London"}
print(locations[(51.5, -0.1)])
def min_max(numbers):
return min(numbers), max(numbers)
lo, hi = min_max([4, 1, 9, 2])
print(lo, hi)
Expected output:
London
1 9
Unpacking works in both directions: x, y = point on the way in, and a tuple of results on the way out. The same pattern appears as object destructuring in objects and JSON and as typed tuples in arrays, tuples and objects.
A worked example: ranking records
Tuples shine when a loop carries a small record with a label attached. This program sorts pairs by their score and unpacks each one as it prints:
students = [("Ada", 91), ("Grace", 88), ("Alan", 72)]
ranked = sorted(students, key=lambda row: row[1], reverse=True)
for position, (name, score) in enumerate(ranked, start=1):
print(f"{position}. {name}: {score}")
Expected output:
1. Ada: 91
2. Grace: 88
3. Alan: 72
Read key=lambda row: row[1] as “sort by the second element”; lambda itself is introduced in chapter 10. The for position, (name, score) in ... line unpacks twice at once: once to get the enumerate pair, and once to split the tuple inside it.
Common mistakes
tasks = tasks.append("x")— mutators returnNone, so your list is replaced byNone; call the method on its own line.- Assuming
other = my_listmakes a copy — it creates a second name for one object; usecopy(),[:]orlist()when you need independence. - Slicing and expecting the original to change — slices always produce a new list; assign the result if you want to keep it.
- Treating a tuple as a list that happens to be small — if you need to append, sort or replace, you wanted a list; convert deliberately with
list(record). - Using a list as a dictionary key — lists are unhashable and raise
TypeError; wrap the values in a tuple instead.
Practice checklist
- Build a list of five numbers, print the first, the last, and every second item using negative indexes.
- Run each method in the table on a practice list and confirm what it returns by printing the result of
sort()andpop(). - Reproduce the aliasing example, then fix it with a copy so
originalstays untouched. - Write a function that returns
min,maxandaverageof a list in onereturn, and unpack all three at the call site. - Convert a list of names to a tuple, prove you cannot edit it, then convert it back with
list().
What's next?
Lists keep values in order; sometimes you need to fetch them by name instead. Chapter 9 builds the language's most-used structure and its fast, unordered sibling in Dictionaries and Sets.
Frequently Asked Questions
Python is a general-purpose language first released in 1991, designed around one idea: code is read far more often than it is written. English keywords and indentation instead of braces make it readable, the standard library covers files, JSON, dates and testing, and pip adds thousands more packages. The same language handles scripts, web APIs, data work and machine learning.
None at all. Python is widely considered the friendliest first language: no declaration keywords, no braces, no build step, and error messages that name the problem. This series begins at your first print() call. If you already read JavaScript or TypeScript you will adjust quickly, but no previous language is assumed anywhere in these 24 chapters.
Python 3 only, any release from 3.12 onwards. Python 2 reached end of life in 2021 and its print "hi" statements and xrange() no longer run. One historical quirk remains: installers ship the interpreter as python3 on macOS and Linux, while Windows usually offers python or the py launcher, and every command in this series works with that one-word swap.
More than you might expect. Everyday automation such as renaming files, cleaning spreadsheets or scraping pages; web backends with Django, Flask or FastAPI; data analysis and machine learning with pandas, NumPy and PyTorch; DevOps tooling, desktop apps and games. Python is also the classic glue language, calling C, Rust or Fortran libraries from short readable scripts.
Pure Python can run 10 to 100 times slower than C, yet that rarely blocks you: heavy numeric work lives in C extensions such as NumPy, which Python coordinates. Dynamic typing does push errors to runtime, surfacing as a TypeError on the line that finally uses the bad value. Type hints checked by mypy move those mistakes back into your editor.
Only the interpreter. Install CPython from python.org, version 3.12 or newer, confirm it with python3 --version, and open a plain text editor; VS Code with the Python extension is the recommendation here. The early chapters use no third-party packages, so there is nothing else to set up until virtual environments and pip arrive.
A dict is a real runtime mapping: keys may be any hashable value rather than only strings, insertion order has been guaranteed since Python 3.7, and a missing key raises KeyError unless you ask with get(). JavaScript objects coerce every key to a string and carry a prototype chain. A TypeScript interface is erased at compile time and describes shape only; TypedDict is Python's checked equivalent.
Reach for a list when items will be added, removed or reordered: it is ordered, mutable and rich with methods. Pick a tuple for fixed records and function returns, because it is immutable and hashable, so it can be a dictionary key. Choose a set for unique values and fast membership checks; it has no order and de-duplicates on creation.
Yes, gradually. Annotate public function signatures first; the interpreter ignores hints at runtime, so a checker such as mypy is what gives them teeth. Add tests the same way: pytest collects any test_*.py file and plain assert statements, and a suite that finishes in a tenth of a second is one you will run after every change. Both habits cost little now and much more later.
JavaScript and TypeScript keep the browser, so anything rendered to the user stays theirs. Python takes the server: Django for full-featured applications, Flask for small ones, FastAPI for typed async APIs where your annotations become automatic documentation. Your TypeScript habits transfer directly, and asyncio mirrors the event loop you already know, just with different keywords.