Lesson 03 / 16
Variables and Data Types
A name carries no type, the object does: a single name binds to seven different types without a declaration, the `+=` notation rebinds the name in three of four types and mutates the object in one, and whether two equal values turn out to be the same object depends on the implementation.
Contents
The previous lesson showed that indentation carries blocks and blocks carry statements, and assignment — the most commonly written statement — showed up in the table only as “not an expression.” This lesson looks at what that statement does.
The Programming Fundamentals course already established that assignment is a
binding, that a variable can be modeled either as a named memory cell or as a label
attached to an object, the distinction between is and ==, and aliasing. None of
that is repeated. What was built there was a concept: a language picks one of two
models. What is measured here is the observable consequences of the model Python
picked — that a name carries no type at all, whether += rebinds the name or mutates
the object depending on the type, and what determines whether two equal values turn out
to be the same object.
A Name Carries No Type
In Python, every value is an object, and type is a property of the object. A name is
not a declaration, only a bond; it neither declares a type nor locks onto one. This is
why the type built-in is asked of the object, not the name.
- LF15 — A value of each of seven distinct types is bound to a single name in turn; the source carries no type declaration at all. What is measured is the type name the object reports after every binding.
- LF16 — The last binding is to a function; because a function is itself a bindable object, it shows up in the same table.
"""A name is a label: the object carries the type, the name carries none.""" VALUES = (12, 3.5, "value", (1, 2), [1, 2], {"north": 1}, None) def length(text): return len(text) measurement = None types = [] for value in VALUES: measurement = value # single name, seven objects in sequence types.append(type(measurement).__name__) print("types bound to a single name in sequence:", " ".join(types)) print("binding count", len(VALUES), "— none of them declares a type") print("the name's type after the last binding:", type(measurement).__name__) measurement = length # same name, now a function print("the same name can also be bound to a function:", type(measurement).__name__, "->", measurement("value"))
types bound to a single name in sequence: int float str tuple list dict NoneType binding count 7 — none of them declares a type the name's type after the last binding: NoneType the same name can also be bound to a function: function -> 5
A single name was bound to 7 distinct types, and to a function on the eighth. No
declaration was written at any binding, because there is nowhere to write one:
measurement is not a container, it is a name pointing at whatever object it is
currently attached to. There is no operation that asks the name’s type either; a type
call goes to the object the name is bound to, and that object gives the answer.
The last line closes one more class of case. A function is an object too; defining one is nothing more than binding a name to an object. That is why a function can be placed in a name, a list, or a dictionary, and called from there.
Python has a notation that looks like a declaration and is prone to confusion: a type name can be written next to a parameter or a return value. This notation is not a declaration, it is an annotation; the interpreter stores it but does not enforce it.
"""An annotation is not a declaration: the interpreter does not enforce what is written.""" def double(n: int) -> int: return n * 2 print("integer:", double(12)) print("string: ", repr(double("ab"))) print("list: ", double([1, 2])) print("where the annotations are stored:", list(double.__annotations__))
integer: 24 string: 'abab' list: [1, 2, 1, 2] where the annotations are stored: ['n', 'return']
Even though the function’s annotation says it expects an integer, it worked with a string and a list too, with no warning at all. The written annotations are not lost either; they sit on the function under two names. So the annotation is written not for execution but for the reader and outside checkers that run separately. Looking at an annotation and assuming “the type is checked here” is one of the most expensive misreadings of the language.
This is called dynamic typing, and it needs to be read correctly: it does not mean
there is no type, it means type checking is deferred to execution time. This is the
same limit as in the first lesson. Whether a name exists could not be known at compile
time; whether an object supports an operation cannot be known either, because the object
only comes into being while running. The exception that appears when a defect is found
is also the same one measured in the first lesson — TypeError if the special method
sought does not exist.
Built-in Types and the Protocol They Carry
Type belongs to the object; so what is it that tells types apart? The Programming Fundamentals course built the basic data types as a classification — number, text, boolean, collection. That classification is not repeated. What is asked here is different, and it grows out of the first lesson’s measure: a built-in type announces which syntax forms it can support through the special methods it defines. The type’s name says nothing; the method list says everything.
- LF17 — Each type is represented by a single example, and the probe is made not on the example itself but on its type; what is asked is whether the type carries that method.
- LF18 — The “mutable” column is a known trait of the type and is not measured; only the method columns in the table are measurements.
"""What built-in types can do: look at the special methods they carry.""" EXAMPLES = (("int", 12, False), ("float", 3.5, False), ("bool", True, False), ("str", "value", False), ("tuple", (1, 2), False), ("list", [1, 2], True), ("dict", {"north": 1}, True), ("set", {1, 2}, True), ("NoneType", None, False)) PROTOCOL = ("__len__", "__iter__", "__contains__", "__getitem__", "__add__") print(f"{'type':<9s} {'mutable':>11s} " + " ".join(f"{p.strip('_'):>10s}" for p in PROTOCOL)) for name, example, mutable in EXAMPLES: supported = " ".join(f"{'yes' if hasattr(type(example), p) else '—':>10s}" for p in PROTOCOL) print(f"{name:<9s} {str(mutable):>11s} {supported}") print() print("is bool an int:", isinstance(True, int), "| True + True =", True + True) big = 2 ** 100 print("an integer's width is not fixed:", big.bit_length(), "bits;", "its square", (big * big).bit_length(), "bits")
type mutable len iter contains getitem add int False — — — — yes float False — — — — yes bool False — — — — yes str False yes yes yes yes yes tuple False yes yes yes yes yes list True yes yes yes yes yes dict True yes yes yes yes — set True yes yes yes — — NoneType False — — — — — is bool an int: True | True + True = 2 an integer's width is not fixed: 101 bits; its square 201 bits
The table groups types not by name but by capability. The three numeric types carry
only __add__: they can be added, but no loop can be built over them, no length can be
asked. str, tuple, and list fill all five columns; the only difference between
them is not in the measured columns but in the mutability column. dict and set
carry the top three protocols but not __add__ — the reason two dictionaries cannot be
merged with + is not a prohibition, it is that the method is not defined.
The NoneType row is empty: none of these forms works on None, and it gives the same
result as the bare object in the first lesson.
The single gap in the set row is instructive too. A set carries __contains__ but not
__getitem__: you can ask whether an item is in it, you cannot ask which position it
sits at. This is the code-level counterpart of a set being “unordered.”
The bottom two rows close two Python-specific points. bool is a subtype of int —
which is why True + True is not a TypeError, it gives 2. And an integer has no
fixed width: a hundred-bit value stops at 101 bits, and its square grows to 201
bits. The How Computers Work course established the overflow behavior of a fixed-width
type; the difference measured here is that Python’s integer has no such limit.
There is no such thing as not fitting, only a representation that grows.
The Binding’s Identity
More than one name can be bound to the same object, and the is operator asks exactly
that. This lesson does not print identity numbers: the number depends on the
environment, changes from run to run, and no program’s correctness should depend on it.
The question to ask is not “which object” but “the same object?” — and only is
answers that.
The Python-specific version of this question shows up in compound assignment. x += extra is a single notation, but it has two different outcomes: it either rebinds the
name to a new object, or mutates the object it is bound to in place. Which one happens
can be measured by attaching a second name to the object.
- LF19 — In every measurement, a value is bound to a name, a second name is
attached to that same object, and
+=is run only on the first name.isis then used to check whether the first name still points at the same object. - LF20 — Four types are chosen so that three are immutable and one is mutable; the result column reflects this trait of the type, not the particular values chosen.
"""Same notation, two different results: was the binding rebuilt, or the object mutated?""" def mutated_in_place(value, extra): x = value witness = x # second name attached to the same object x += extra return x is witness, witness print(f"{'type':<9s} {'after +=':<22s} what the second name sees") for name, value, extra in (("integer", 10, 1), ("string", "ab", "cd"), ("tuple", (1, 2), (3,)), ("list", [1, 2], [3])): same, witness = mutated_in_place(value, extra) print(f"{name:<9s} {'object mutated' if same else 'name rebound':<22s} {witness!r}")
type after += what the second name sees integer name rebound 10 string name rebound 'ab' tuple name rebound (1, 2) list object mutated [1, 2, 3]
In three of four types the name was rebound, in one the object was mutated.
What creates the difference is whether the type is mutable. Integer, string, and
tuple are immutable types: once built, an object’s content cannot be changed by any
route. += cannot perform an in-place operation on such an object; it produces a new
object and binds the name to it, and the second name stays on the old object. The
right-hand column shows this directly — in three rows, the second name still sees the
starting value.
For the list, the result flips. The object itself grows; the name is not touched, because there is no need to touch it. The second name, being bound to the same object, sees the grown version too. The same line meaning two different things for two types is not an inconsistency, it is one rule with two outcomes: an in-place operation cannot be defined on an immutable object.
This is the first sign of the course’s second claim. The notations x = x + extra and
x += extra do not give the same result here, and the difference is not accidental:
the two notations call two different special methods. Their names are set in the
next lesson.
The Interned Object
One question remains: are two equal values the same object? The answer carries far less guarantee than the question suggests.
- LF21 — The compared values are produced at run time, inside a function call; this way, two constants sitting side by side in the source being merged at compile time does not interfere with the measurement.
- LF22 — The “guarantee” column says what the result rests on: the
language definitionrow is the same in every interpreter, theimplementationrow is subject to change. - LF23 — Identity is only asked with
is; no identity number is printed.
"""Interned object: the result is IMPLEMENTATION-DEPENDENT.""" def produce(a, b): return a + b # the value is produced at run time def nothing(): return None def true_value(): return 1 == 1 PAIRS = ( ("small integer", produce(2, 3), produce(1, 4)), ("large integer", produce(500, 1), produce(499, 2)), ("short string", produce("va", "lue"), produce("val", "ue")), ("list", produce([1], [2]), produce([1], [2])), ) print(f"{'pair':<16s} {'==':>6s} {'is':>6s} guarantee") for name, x, y in PAIRS: guarantee = "language definition" if isinstance(x, list) else "implementation" print(f"{name:<16s} {str(x == y):>6s} {str(x is y):>6s} {guarantee}") print("\nsingleton objects the language definition guarantees:") print(" is a None returned from elsewhere the same object:", nothing() is None) print(" is a boolean returned from elsewhere the same object:", true_value() is True)
pair == is guarantee small integer True True implementation large integer True False implementation short string True False implementation list True False language definition singleton objects the language definition guarantees: is a None returned from elsewhere the same object: True is a boolean returned from elsewhere the same object: True
== gives True for all four pairs: the values are equal. The is column is where
it scatters. For the small integer, two separate computations gave the same object; for
the large integer and the short string, they did not.
These three rows are implementation-dependent and have to be read that way. An
interpreter can build certain frequently used immutable objects ahead of time and hand
out the same one every time it is requested; this is called interning. Which values
fall under this is not written in the language’s definition. Getting True on all three
of these rows in a different interpreter — or even a different run of the same
interpreter — or False on all three, breaks nothing in the language’s definition. The
table describes a behavior, it does not establish a rule.
The fourth row is a different class and does carry a guarantee: a list literal builds a new object every time it is evaluated. Interning is not even in question here, because the object is mutable — if it were shared, a change made in one would show up in the other.
A single practical rule follows from this, and it has no exception: equality is asked
with ==, is is only for identity. Code that tests whether two values are the same
using is works on small values and silently gives the wrong answer on large ones — a
defect class that raises no error message and is among the hardest to find.
The last two lines show the rule’s legitimate use. None is a single object by the
language’s definition; wherever it comes from, it is the same object. The same holds
for boolean values. This is why whether a value is empty is asked not with == None
but with is None: what is really being asked here is identity, and the language
guarantees the answer.
Summary
- Type is a property of the object, not the name; a single name can be bound to 7 distinct types and a function with no declaration at all.
- Dynamic typing is not the absence of type but type checking deferred to execution
time; an unsupported operation falls with
TypeError. - The
+=notation rebinds the name to a new object in 3 of four types and mutates the object in place in 1; what makes the difference is whether the type is mutable. - An in-place operation cannot be defined on an immutable object; this is why a second name keeps seeing the old value.
- Whether two equal values turn out to be the same object depends on the implementation, and a program’s correctness must not rest on it; a list literal building a new object every time, by contrast, is the language definition’s guarantee.
- Equality is asked with
==;isis only for identity — which is why aNonetest is written asis None.
Next Step
This lesson measured that the notation x += extra does two different jobs depending
on the type, but never gave that job a name. The next lesson names it and pays off
the course’s second claim: n + m and n += m are not two spellings of the same
operator, they are calls to two different special methods. Which method does each
arithmetic, comparison, logical, and membership operator call, where does a membership
test fall when it cannot find the method it is looking for, and what happens when an
operator finds no match at all?
To keep your progress and take notes, Log in
My notes
Log in to take notes.