Phase 1: Python for AI Engineering · 60 min · Python · Type hints · List comprehensions
The Concept
Variables are labels, not boxes
The single most important idea in this lesson: a variable in Python is a name pointing at an object, not a box that holds a value. Assigning with = doesn't copy anything — it just sticks another label on the same object.
a = [1, 2, 3] # create a list object; 'a' points at it
b = a # 'b' points at the SAME object (no copy!)
b.append(4) # we change the one shared object
print(a) # [1, 2, 3, 4] ← 'a' sees it too
People expect a to stay [1, 2, 3]. It doesn't, because a and b are two names for one list. This is the "references, not copies" rule, and it causes more beginner bugs than anything else.
x = [10, 20]
y = x
y.append(30)
print(x)
y = x makes y a second label on the same list. Appending through y changes the object both names point to, so x shows [10, 20, 30] too.
When you genuinely want a separate copy, ask for one:
a = [1, 2, 3]
c = a.copy() # a brand-new list with the same contents
c.append(4)
print(a) # [1, 2, 3] ← unchanged
print(c) # [1, 2, 3, 4]
The rule
= never copies. It points a name at an object. To get an independent list, use .copy() (or list(a) / a[:]).
Write a function `deep_copy_list` that takes a list and returns a **new** list with the same elements. Modifying the returned list must NOT affect the original.
~~~
def deep_copy_list(lst):
# Your code here — return a copy of lst
pass
~~~
original = [1, 2, 3]
copy = deep_copy_list(original)
copy.append(4)
assert original == [1, 2, 3], f"Original should be unchanged, got {original}"
assert copy == [1, 2, 3, 4], f"Copy should have 4 appended, got {copy}"
print("deep_copy_list OK")
Everything is an object with a type
Every value in Python — even a plain number — is an object, and every object has a type. The type decides what you can do with the value. You can always ask:
print(type(42)) # <class 'int'>
print(type(3.14)) # <class 'float'>
print(type("hello")) # <class 'str'>
print(type(True)) # <class 'bool'>
print(type([1, 2, 3])) # <class 'list'>
print(type(None)) # <class 'NoneType'>
The five types you'll use constantly:
| Type | Example | Mutable? | Used for |
|---|
int | 42 | no | counts, indices |
float | 3.14 | no | measurements, weights, losses |
str | "gpt" | no | text, labels |
list | [1, 2, 3] | yes | ordered, changeable sequences |
dict | {"lr": 0.01} | yes | key → value lookups (configs) |
"Mutable" means can be changed in place after creation. Lists and dicts can; ints, floats, and strings cannot — operating on them produces a new object. That's the deeper reason the copy rule above only bites you with lists and dicts.
A function receives a list and appends to it. The caller's original list also changes. But when the same function receives a string and concatenates to it, the caller's original string is unchanged. Why?
Python passes everything the same way — by reference. The difference is mutability: list.append() mutates the existing object (caller sees the change), while s += "x" creates a new string object that the caller never receives. This is why the reference trap only bites you with mutable types.
Truthiness: what counts as True
Python lets you put almost anything in an if. Behind the scenes it asks "is this truthy?" You need to know which values are falsy (treated as False):
# Everything here is falsy:
bool(False) # False
bool(0) # False
bool(0.0) # False
bool("") # False (empty string)
bool([]) # False (empty list)
bool({}) # False (empty dict)
bool(None) # False
Everything else is truthy — including some surprises:
print(bool("False")) # a non-empty string
print(bool([0])) # a list containing a falsy item
print(bool(" ")) # a single space
Truthiness is about emptiness, not content. "False" is a non-empty string, [0] is a non-empty list, and " " is a non-empty string — all truthy. Only genuinely empty / zero / None values are falsy.
This lets you write clean checks. To test "does this list have anything in it?", just use the list itself:
results = []
if results: # truthy only if non-empty
print("We have results")
else:
print("No results yet") # ← this runs
Don't confuse "empty" with "None"
if x: is False for both an empty list [] and for None. If you specifically need to know whether something was never set, check if x is None: explicitly — they mean different things.
== vs is
These look similar and trip up almost everyone:
== asks "are the values equal?" — use this ~99% of the time.is asks "are these the exact same object in memory?" — use it only for None.
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True — same contents
print(a is b) # False — two different list objects
The only rule you need
Use == to compare values. Use is only with None, like if x is None:. Never use is to compare numbers or strings — it sometimes appears to work by accident, which makes the eventual bug brutal to find.