Skip to content
academia.sh

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. is is 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 definition row is the same in every interpreter, the implementation row 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 ==; is is only for identity — which is why a None test is written as is 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.

Start typing to search.

↑↓ Esc navigate · open · close