Python Lists: Everything You Need to Know

Lists are Python's most versatile built-in data structure. They store an ordered collection of values β€” any mix of types β€” and they grow or shrink dynamically as you add and remove items. If you only ever learn one data structure in Python, make it the list. It will show up in virtually every program you write.

Creating Lists

Use square brackets [] to create a list literal, or list() to convert another iterable into a list.

python
# List literal
crops = ["wheat", "carrot", "tomato"]

# Mixed types (valid but uncommon in practice)
mixed = [42, "hello", True, 3.14, None]

# Empty list
inventory = []

# From another iterable
digits = list(range(5))   # [0, 1, 2, 3, 4]
chars = list("farm")      # ['f', 'a', 'r', 'm']

print(crops)    # ['wheat', 'carrot', 'tomato']
print(len(crops))   # 3

Indexing and Slicing

Lists are zero-indexed β€” the first item is at index 0. Negative indices count from the end. Slicing extracts a sub-list using [start:stop:step].

python
values = [10, 25, 15, 40, 30, 5]

print(values[0])     # 10  β€” first item
print(values[-1])    # 5   β€” last item
print(values[1:4])   # [25, 15, 40]  β€” index 1 up to (not including) 4
print(values[:3])    # [10, 25, 15]  β€” from start to index 3
print(values[3:])    # [40, 30, 5]   β€” from index 3 to end
print(values[::2])   # [10, 15, 30]  β€” every other item
print(values[::-1])  # [5, 30, 40, 15, 25, 10]  β€” reversed
Slice syntax: list[start:stop:step]. Omitting start defaults to 0; omitting stop goes to the end; omitting step defaults to 1.

Common Methods

Lists come packed with methods for adding, removing, searching, and sorting items. Here are the ones you'll use most often:

python
plots = ["A1", "B2", "C3"]

plots.append("D4")          # add to end: ["A1","B2","C3","D4"]
plots.insert(1, "X0")       # insert at index 1
plots.remove("B2")          # remove first occurrence of "B2"
last = plots.pop()          # remove and return last item
plots.sort()                # sort in place (alphabetically)
plots.reverse()             # reverse in place
idx = plots.index("A1")     # find index of first "A1"
count = plots.count("A1")   # how many times "A1" appears
plots.clear()               # remove all items

# Non-mutating alternatives
crops = ["tomato", "wheat", "carrot"]
sorted_crops = sorted(crops)        # returns new sorted list
print(sorted_crops)   # ['carrot', 'tomato', 'wheat']
print(crops)          # unchanged

len() and the in Operator

len() returns the number of items. The in keyword tests membership.

python
seeds = ["wheat", "carrot", "pumpkin"]

print(len(seeds))             # 3
print("carrot" in seeds)      # True
print("tomato" in seeds)      # False
print("tomato" not in seeds)  # True

# Guard against adding duplicates
new_crop = "wheat"
if new_crop not in seeds:
    seeds.append(new_crop)
else:
    print(f"{new_crop} already in inventory")

Iterating Over a List

The most common pattern β€” use for item in list. When you need the index too, use enumerate().

python
crops = ["wheat", "carrot", "tomato"]

for crop in crops:
    print(f"Watering {crop}")

# With index
for i, crop in enumerate(crops):
    print(f"Plot {i}: {crop}")

2D Lists β€” List of Lists

A list that contains other lists represents a 2D grid β€” perfect for a farm map. Access cells with two indices: grid[row][col].

python
# 3x3 grid β€” None means empty
grid = [
    [None,    "wheat",  None   ],
    ["carrot", None,   "tomato"],
    [None,    "carrot", None   ],
]

print(grid[0][1])   # "wheat"  β€” row 0, col 1
print(grid[1][2])   # "tomato" β€” row 1, col 2

# Count planted plots
planted = sum(1 for row in grid for cell in row if cell is not None)
print(f"Planted: {planted} plots")

List Comprehensions

A list comprehension builds a new list in a single readable expression. The syntax is [expression for item in iterable if condition] β€” the if part is optional.

python
nums = [1, -3, 5, -2, 8, 0]

# Keep only positive numbers, doubled
result = [x * 2 for x in nums if x > 0]
print(result)   # [2, 10, 16]

# Extract names of mature crops
plots = [
    {"name": "wheat",  "mature": True},
    {"name": "carrot", "mature": False},
    {"name": "tomato", "mature": True},
]
ready = [p["name"] for p in plots if p["mature"]]
print(ready)   # ['wheat', 'tomato']

# Build a flat list of (row, col) tuples for a 3x3 grid
coords = [(r, c) for r in range(3) for c in range(3)]
print(coords[:4])   # [(0,0),(0,1),(0,2),(1,0)]

Copying Lists

Simply writing b = a doesn't copy a list β€” both variables point to the same object. Use slicing, .copy(), or list() for a shallow copy. For nested structures (list of lists), use copy.deepcopy().

python
original = ["wheat", "carrot"]

# NOT a copy β€” both names point to the same list
alias = original
alias.append("tomato")
print(original)   # ['wheat', 'carrot', 'tomato']  ← modified!

# Shallow copy β€” changes to top-level items don't affect original
shallow = original[:]          # or original.copy() or list(original)
shallow.append("pumpkin")
print(original)   # ['wheat', 'carrot', 'tomato']  ← unchanged

# Deep copy for nested lists
import copy
grid_a = [[1, 2], [3, 4]]
grid_b = copy.deepcopy(grid_a)
grid_b[0][0] = 99
print(grid_a[0][0])   # 1  β€” unaffected

Practical: Crop Positions as a List of Tuples

A clean way to track planted crops in GrowBit is to keep a list of (x, y) tuples. This is lightweight, easy to iterate, and simple to query.

python
planted_positions = [(0, 0), (1, 0), (1, 2), (3, 3)]

def plant_at(x, y, positions):
    if (x, y) not in positions:
        positions.append((x, y))
        print(f"Planted at ({x},{y})")
    else:
        print(f"({x},{y}) already occupied")

def harvest_at(x, y, positions):
    if (x, y) in positions:
        positions.remove((x, y))
        print(f"Harvested ({x},{y})")
    else:
        print(f"Nothing at ({x},{y})")

plant_at(2, 2, planted_positions)    # Planted at (2,2)
harvest_at(1, 0, planted_positions)  # Harvested (1,0)
print(planted_positions)