Creating and Using Lists

A list is Python's most-used container: an ordered, mutable sequence that can hold any mix of values. You create one with square brackets, [1, 2, 3], or by calling the built-in list() on any iterable. Because lists grow and shrink cheaply, they are the default choice any time you need to collect items in order before deciding what to do with them.

Lists are defined by their index positions. The first item is at index 0, the last is at index -1, and you can pass two indexes separated by a colon to get a slice: nums[1:4]. Slicing always returns a new list and never raises an error for out-of-range bounds, which makes it safe inside larger expressions.

Unlike arrays in some other languages, a Python list does not care about the types of its elements. You can mix integers, strings, other lists, and even functions in the same list. That flexibility is convenient but also a warning: in production code, a list whose items all share the same type is easier to reason about, and type checkers like mypy will enforce it via list[int] annotations.

Creating a list is only half the story. Python gives you a rich set of methods (.append, .extend, .insert, .remove, .pop, .sort) that modify the list in place, plus free functions (len, sorted, sum, min, max) that take a list and return a fresh value. Knowing which methods mutate and which do not is the difference between a bug-free loop and a confusing one.

Four ways to build a list

Use a literal [1, 2, 3] when you know the values at source level. Use list(iterable) to materialize a generator or a range: list(range(5)) is [0, 1, 2, 3, 4]. Use a list comprehension [x*x for x in range(5)] when each item is computed from another sequence. For an empty list you can write either [] or list(); the literal is slightly faster.

Repeating an item is written [0] * 4 for simple values. Be careful: [[]] * 3 creates three references to the same inner list, so appending to one changes all three. Use [[] for _ in range(3)] when you need independent inner lists.

Lists are references, not values

Assigning one list to another name makes both names point at the same underlying object. Mutating through one name changes what the other sees. To get an independent copy use new = old[:], new = old.copy(), or new = list(old); all three create a shallow copy. If the list contains other lists, reach for copy.deepcopy() from the standard library.

This reference behavior also matters when you pass a list into a function. The function can modify the list the caller passed in. If you want to play it safe, document the intent clearly or pass my_list.copy().

These are the core list operations you will use on a daily basis.

ToolPurpose
list.append(x)
method
Adds a single item to the end of the list.
list.extend(it)
method
Appends every element from another iterable.
list.insert(i, x)
method
Inserts x before position i (can be slow for large i).
seq[i:j]
slicing
Returns a new list from index i (inclusive) to j (exclusive).
len(seq)
built-in
Number of items currently in the list.
[expr for x in it]
list comprehension
Builds a new list element by element.
sorted(seq)
built-in
Returns a sorted copy without mutating the input.
copy.deepcopy(obj)
standard-library function
Deep-copies nested structures safely.

Creating and Using Lists code example

The script below explores every common way of creating a list and shows how references behave when you assign or copy.

# Lesson: Creating and Using Lists
from copy import deepcopy

literal = [10, 20, 30]
from_range = list(range(0, 10, 2))
squares = [x * x for x in range(1, 6)]
grid = [[0 for _ in range(3)] for _ in range(2)]
empty: list[int] = []

print("literal:   ", literal)
print("from_range:", from_range)
print("squares:   ", squares)
print("grid:      ", grid)
print("empty:     ", empty, "len=", len(empty))

original = [1, 2, 3]
alias = original
shallow = original[:]
nested = [[1, 2], [3, 4]]
deep = deepcopy(nested)

alias.append(99)
shallow.append(100)
nested[0].append(5)

print("original:", original)  # aliased write is visible
print("shallow: ", shallow)   # shallow copy is independent
print("deep:    ", deep)      # deep copy did not see the mutation

Here is what to notice as you read the script:

1) Four construction styles cover almost every real use: literal, list(range), comprehension, nested.
2) `alias = original` never copies; both names point at the same list.
3) `original[:]` and `.copy()` duplicate the outer list only; inner objects are still shared.
4) `deepcopy` is the right tool when the list contains other mutable objects.

Practice shallow vs. deep copy and a comprehension built from a range.

# Example A: safe shared-default pattern
rows = [[0] * 4 for _ in range(3)]   # 3 independent rows of 4 zeros
rows[0][0] = 1
print(rows)   # only rows[0] changed

# Example B: filter with a comprehension
nums = list(range(20))
evens = [n for n in nums if n % 2 == 0]
print(evens)

These assertions confirm the mutability rules you just saw.

assert [0] * 3 == [0, 0, 0]
assert list(range(3)) == [0, 1, 2]
assert [x * 2 for x in range(3)] == [0, 2, 4]
assert len([[] for _ in range(3)]) == 3

Running the script prints something like:

literal:    [10, 20, 30]
from_range: [0, 2, 4, 6, 8]
squares:    [1, 4, 9, 16, 25]
grid:       [[0, 0, 0], [0, 0, 0]]
empty:      [] len= 0
original: [1, 2, 3, 99]
shallow:  [1, 2, 3, 100]
deep:     [[1, 2], [3, 4]]