How it works
The explanation, with examples you can run.
Everything is an object
Every object has three things: an identity (fixed for its lifetime; id() returns it, and in CPython it is the memory address), a type (which decides what operations it supports) and a value. That really does mean everything: type(len) is builtin_function_or_method, type("abc") is str, and type(str) is type. Functions, classes and modules are objects you can store in a dict or pass to another function, which is what makes decorators and plugin registries possible.
Names are arrows
a = [1, 2]b = a # a second name for the same listb.append(3)print(a, a is b) # [1, 2, 3] True c = list(a) # a new list with the same itemsprint(c == a, c is a) # True Falseb = a did not copy the list; it made a second arrow to the same object, so a change through b is visible through a. list(a) builds a new object, so it is equal (==) but not identical (is). Function calls work the same way: the parameter is a new name for the caller's object. If the function mutates it, the caller sees the change; if the function rebinds the name, it doesn't.
Mutable and immutable
Lists, dicts, sets and most user-defined objects are mutable: they can change in place. Ints, floats, strings, tuples and frozensets are immutable: any "change" builds a new object. Immutability is shallow, though. A tuple can hold a list, and that list can still change. That leads to a famous oddity: t = (1, [2]); t[1] += [3] raises TypeError: 'tuple' object does not support item assignment, yet afterwards t is (1, [2, 3]). The list was extended in place first, and only the step that stores the result back into the tuple failed.
Augmented assignment is where the difference shows. For a list, x += [3] extends it in place (every alias sees it); for an int or a tuple there is no in-place version, so x += 1 builds a new object and rebinds only that name.
Default arguments are shared
def add_item(x, items=[]): items.append(x) return items print(add_item(1)) # [1]print(add_item(2)) # [1, 2]: the default list is shared between callsThe default list is created once, when def runs, and stored on the function. Every call that omits items mutates that same list. The fix is items=None and if items is None: items = [] inside the body.
is vs ==
== calls the type's __eq__ and compares values. is compares identities and can't be overridden. Use is only for singletons like None, True, False or a sentinel object you created. CPython happens to reuse some small ints and strings, so x is 5 may appear to work and then fail for larger numbers (1000 is int("1000") is False). Python 3.8 and later warn about is with a literal for exactly this reason.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.