What’s one thing you learned? What’s still confusing?
Python Internals: CPython, Bytecode & Memory
AST, bytecode, refcounting, cyclic GC, the GIL, integer caching, and cProfile.
Async/Await & Concurrency: Parallel Python
Threading, multiprocessing, asyncio, and concurrent.futures.
Algorithms & Complexity: Sorting, Searching & Big O
Big O, all sorting algorithms, binary search, two pointers, sliding window.
Interactive Labs for This Track
Loop Visualizer
You're a factory robot repeating the same task on an assembly line — watch how loops automate repetitive work
List Slicing
You have a playlist of 50 songs — grab just tracks 10 through 20 with a single slice expression
Sorting Algorithms
You're organizing a library of 10,000 books — which sorting method is fastest?
Ask questions, share insights
Optional deep-dive — You can write working AI apps and call APIs without internalizing this lesson. Skip ahead if you're under a deadline, and return when you next hit "why did mutating this list change that other variable?" — this lesson is the answer.
Relax! This lesson covers a concept that confuses even experienced developers. Take your time — understanding this deeply will save you hours of debugging later.
int x = 42 stores the value directly in memory. Python is different. Every value is a full object with three properties: a type, an identity (memory address), and a value.# Every value has a type
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'># Every object has a unique identity (memory address)
x = 42
print(type(x)) # <class 'int'>
print(id(x)) # e.g., 140234567 (unique address)
print(x) # 42 (the value)This is the most important concept in Python's memory model. Variables do not contain values -- they point to objects.
a = [1, 2, 3]
b = a # b points to the SAME list object
b.append(4)
print(a) # [1, 2, 3, 4] -- a changed too!
print(b) # [1, 2, 3, 4]
print(a is b) # True -- same object in memoryStep through it visually. Below is an animated names region (left) and heap (right). Watch the green binding arrow form when you sayx = 5, watch a second arrow appear (pointing at the SAME object!) when you sayy = x, then watch the arrow MOVE when you rebindx. Preset D shows the mutation case, and preset E shows the augmented-assignment trap where you accidentally create a new list.
See it happen! Use the step-through debugger below — click Run, then Step Forward to watch each line execute and see variables change in real time.
What does this print? x = [1,2]; y = x; y = [3,4]; print(x)
y = [3, 4]) peels the label "y" off the old list and sticks it on a new list. The old list [1, 2] still exists with "x" pointing to it.HitUnboundLocalError: local variable referenced before assignment? Python decides a name is local at compile time if you assign to it anywhere in the function — even after the read. Usenonlocalorglobal, or rename. See the error decoder.
a and b become two labels on the same list. Mutating through a makes b "see" the change — and a is b is True, because they point to one object, not two.Edit the code, then click Trace it. Python actually runs in your browser — every line, every variable, every print.
Click Trace it to capture the execution trace. The scrubber below will let you step through every variable change line-by-line.
Python has two ways to compare:
== checks equality -- do they have the same value?is checks identity -- are they the exact same object in memory?x = [1, 2, 3]
y = [1, 2, 3]
print(x == y) # True -- same value
print(x is y) # False -- different objects!
print(id(x)) # e.g., 140234567
print(id(y)) # e.g., 140234999 (different address)
z = x
print(x is z) # True -- z is an alias for x# Immutable: int, str, tuple, float, bool, frozenset
s = "hello"
# s[0] = "H" # TypeError! Strings are immutable.
s = "Hello" # Creates a NEW string object. Old "hello" is abandoned.
# Mutable: list, dict, set
lst = [1, 2, 3]
lst.append(4) # Modifies the SAME object in place.
print(lst) # [1, 2, 3, 4]42 points to the same object.# Small integers are cached
a = 100
b = 100
print(a is b) # True -- same cached object
# Large integers are NOT part of the small-int cache. But equal literals in the
# same code object (a module, or one function body) are deduplicated by the
# compiler, so this still prints True:
a = 1000
b = 1000
print(a is b) # True -- one shared constant, not the small-int cache
# Build them at runtime instead and the objects really are distinct:
a = int("1000")
b = int("1000")
print(a is b) # False -- different objects
print(a == b) # True -- same value
print(a == b) # True -- same value though!# String interning: short strings may be cached
x = "hello"
y = "hello"
print(x is y) # True (interned by CPython)
x = "hello world!"
y = "hello world!"
print(x is y) # May be True or False (implementation detail)a = 256; b = 256; print(a is b) then a = 257; b = 257; print(a is b)
Good news: You NEVER need to memorize which numbers are cached. Just remember: always use==for comparing values andisonly forNone. That's it!
Python uses reference counting to manage memory. Each object tracks how many variables point to it. When the count reaches zero, the memory is freed.
import sys
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2 (a + the argument to getrefcount)
b = a
print(sys.getrefcount(a)) # 3 (a + b + the argument)
del b
print(sys.getrefcount(a)) # 2 (back to a + the argument)Three presets to try above: A simple refcount drops to 0 and the object dissolves; B twoNodes reference each other and refcounting alone can't free them — watch the sad-face leak indicator appear; Cgc.collect()runs, the scanner sweeps the heap, the unreachable cycle lights up amber, and both objects free at once.
None object, ONE True object, and ONE False object in the entire Python process.x = None
y = None
print(x is y) # True -- same singleton object
print(id(None)) # one fixed address for the entire process
# This is why 'is' is correct for None checks
result = None
if result is None:
print("No result") # Correct and Pythonicimport copy
original = [1, 2, [3, 4], [5, 6]]
# ALIAS -- NOT a copy
alias = original
alias.append(99)
print(original) # [1, 2, [3, 4], [5, 6], 99] -- changed!
# Reset
original = [1, 2, [3, 4], [5, 6]]
# SHALLOW COPY -- new outer list, inner objects shared
shallow = original.copy()
shallow.append(99)
print(original) # [1, 2, [3, 4], [5, 6]] -- outer unaffected
shallow[2].append(99)
print(original) # [1, 2, [3, 4, 99], [5, 6]] -- inner changed!
# Reset
original = [1, 2, [3, 4], [5, 6]]
# DEEP COPY -- completely independent at every level
deep = copy.deepcopy(original)
deep[2].append(99)
print(original) # [1, 2, [3, 4], [5, 6]] -- completely unaffected`a = [[1,2],[3,4]]`. You write `b = a.copy()` then `b[0].append(99)`. What does `print(a)` show?
Tests · Run the code. Try the solution for bonus challenges.
is checks identity (same object), == checks equality (same value) -- use is only for None/True/Falseis for value comparisonsa = [1, 2, 3]; b = a; b.append(4); print(len(a))