---
title: 'Variables and Data Types'
source: 'https://academia.sh/en/courses/python-fundamentals/variables-and-data-types'
course: 'Python Fundamentals'
language: en
updated: '2026-08-17T18:10:28+00:00'
license: 'CC BY-SA 4.0'
---

# 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.

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.

```python
"""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.

```python
"""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.

```python
"""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.

```python
"""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.

```python
"""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?
