Things Python does that are easy to assume work one way, but actually work another. These explanations use a more accurate model of how Python works internally, rather than simplified exam-ready assumptions. ## Everything is an object All values in Python are [[object|objects]]. This includes basic [[data type|data types]] such as `int` and `str`, as well as more complex values like lists, [[subprogram|subprograms]], and [[class|classes]]. This does not mean you should treat classes and objects as the same thing in exam answers. They are conceptually different. The point here is simply that, underneath the surface, Python represents all values as objects. This has important consequences explored in other quirks below. ## Assignment rebinds identifiers, it doesn't modify objects In Python, assignment does not change the value of an existing object. Instead, it makes an [[identifier]] refer to a different [[object]]. Consider: ```python x = 5 x = x + 1 ``` This does not modify the integer `5`. Instead: - `x + 1` creates a new integer object `6`; - `x` is then rebound to refer to `6`; and - the original object `5` is unchanged. You can see this more clearly here: ```python x = [1, 2, 3] y = x x = [9, 9, 9] print(y) # [1, 2, 3] ``` Even though `y = x` initially makes both identifiers refer to the same list, the later assignment `x = [9, 9, 9]` does not modify the original list. It simply rebinds `x` to a new list object and `y` continues to refer to the original list. This applies when assignment is used with expressions that create new objects, including the `+` operator with strings: ```python name = "Jonah" name = name + " Whale" ``` The original `"Jonah"` string is unchanged. `name` has been rebound to refer to the new string: `"Jonah Whale"`. ## Parameters receive references to objects, not copies When you pass a [[mutable]] [[object]] to a [[parameter]], any changes made in the [[subprogram]] affect the original object. This is because parameters behave like assignment (where `parameter = argument`) and [[#Assignment rebinds identifiers, it doesn't modify objects|assignment rebinds identifiers]]. As [[#Everything is an object|everything is an object]]—including `ints`, `floats`, and `str`s—but not all objects are mutable, some objects can change in-place and others cannot. Lists are the most commonly discovered example of mutable objects. Consider this code: ```python def change(items): items.append(4) numbers = [1, 2, 3] change(numbers) print(numbers) # [1, 2, 3, 4] ``` When you pass an object as an argument, the parameter refers to the same object—literally the same object [[instance]]. If that object is mutable, the subprogram can modify it, and those changes will be visible after the subprogram returns. This code works because `.append()` modifies the original list. Contrast that with: ```python def change(items): items = [4] numbers = [1, 2, 3] change(numbers) print(numbers) # [1, 2, 3] ``` Here, `items = [4]` does not modify the original list. Instead, it rebinds the local [[identifier]] `items` to a new list object. The original object is unchanged. Immutable objects, like strings, remain unchanged because they are immutable: ```python def shout(text): text = text.upper() message = "hello" shout(message) print(message) # "hello" ``` `.upper()` returns a new string, it does not modify the original[^1]. ## All subprograms return `None` by default If a subprogram has no `return` statement, or a `return` with no value, Python still returns something: `None`. There's no such thing as a Python subprogram that returns "nothing"—it always returns _something_, even if that something is `None`. `None` is a special value that is a stand-in representing the absence of another value. ```python def greet(name): print(f"Hello, {name}!") result = greet("James") print(result) # None ``` An amusing implication of this is that, strictly speaking, there is no such thing as a [[procedure]] in Python. ## Constructors return `None`, not the object `__init__()` is responsible for initialising an object, not creating it—Python has already created the object by the time `__init__()` runs. Because of this, `__init__()` is not allowed to return anything other than `None`; returning anything else raises a `TypeError`. For the purposes of A-level exams, this detail is unimportant and we can pretend that `__init__()` returns the object. Don't do this: ```python class BankAccount: def __init__(self, p_owner, p_initial_balance): self.owner = p_owner self.__balance = p_initial_balance return self # TypeError: __init__() should return None ``` ## Notes [^1]: See [[#Assignment rebinds identifiers, it doesn't modify values]].