Skip to content
Unit 5 of 28

Values, names and types

Contents · 25

Every Python program is about values: numbers, pieces of text, lists of things. This unit explains what a value really is (an object), how names refer to objects, and what the built in types are and how they behave. You will also meet the traps that catch people coming from other languages: two names silently sharing one list, is that "works" in a quick test and fails in production, and 0.1 + 0.2 not being 0.3.

The model you build here, names point at objects, is the key to the rest of the course. Function arguments, copying, default values and many bugs only make sense once you have it.

You will be able to:

  • Explain how names refer to objects, and inspect objects with id, type and isinstance
  • Choose correctly between == and is, and test for None the idiomatic way
  • Classify the built in types as mutable or immutable, hashable or unhashable
  • Work with int, float, Decimal, Fraction, complex, bool and None, and avoid their traps
  • Convert between types, name things according to PEP 8, and use every form of assignment
  • Copy nested data correctly with shallow and deep copies

Objects and names

Everything Python works with is an object: numbers, strings, lists, functions, even types themselves. Every object has three things:

  • an identity, which never changes while the object exists;
  • a type, which decides what the object can do, and which also never changes;
  • a value, which may or may not be changeable, depending on the type.

A name (what other languages call a variable) is a label that refers to an object. Assignment with = binds a name to an object. It never copies anything. That is why the code below creates only two lists, not three:

Rendering diagram…

a and b are two names for list #1, so a change made through b is visible through a. c refers to a different list that happens to hold equal values. Names don't have types; objects do. A name can be rebound to an object of any type at any time.

id, type and isinstance

Three built in functions let you inspect an object:

  • id(obj) returns an integer that is unique among the objects that exist at the same time. In CPython it happens to be the memory address, but that is an implementation detail, and an id can be reused after its object is gone. Compare ids; don't print them.
  • type(obj) returns the object's class. type is itself a class, not a method.
  • isinstance(obj, cls) asks "is this object an instance of cls, or of a subclass of it?". It accepts a tuple of classes. Prefer it to type(x) == int when you check types, because it respects subclasses.

Pitfall. The last line printed True: bool is a subclass of int, so isinstance(True, int) is true. If your code must reject booleans where it expects numbers, check for bool first.

Equality and identity: == versus is

  • a == b asks whether the values are equal. Each type decides what "equal" means.
  • a is b asks whether both names refer to the same object.

Use == for comparing values, which is almost always what you want. Use is only to check for a singleton, an object of which only one exists. The common case is None: write if result is None:.

Equal values are not guaranteed to be the same object. Sometimes they are, which is exactly what makes is dangerous for value comparisons:

The third line is the trap. CPython keeps one shared object for each small integer (from −5 to 1024 in 3.15), and it interns some strings, keeping a single shared copy of, for example, short literals that look like names. A quick test with small values seems to show that is compares values; with larger or computed values it doesn't. The exact rules are an implementation detail that has changed between versions, so never rely on them.

Pitfall. Never use is to compare numbers, strings or other values. Python even warns you: x is 7 with a literal produces a SyntaxWarning. Use ==, and keep is for None (and the rare sentinel object you create yourself).

Mutable and immutable types

An object is mutable if its value can change after it is created, and immutable if it can't. An operation that seems to change an immutable object actually creates a new one. The table also shows which types are hashable, which you will need for dictionary keys and sets (the section on hashability below explains it).

TypeExample literalMutable?Hashable?
int42, 0xFF, 1_000_000noyes
float3.14, 1e-3noyes
complex2+3jnoyes
boolTrue, Falsenoyes
NoneTypeNonenoyes
str"text", 'text'noyes
bytesb"raw"noyes
tuple(1, "a"), (1,)noonly if all its items are hashable
frozensetfrozenset({1, 2})noyes
rangerange(10)noyes
list[1, 2]yesno
dict{"k": 1}yesno
set{1, 2} (set() when empty)yesno
bytearraybytearray(b"raw")yesno

A set is mutable, but each of its elements must be hashable. The standard library also offers array.array, a mutable sequence of numbers of one type; it isn't a built in type, so you import it from the array module.

You can see the difference with id. Adding to a string builds a new string and rebinds the name; adding to a list with += changes the same list:

Trying to change part of an immutable object is an error:

The error message is:

An immutable container can still hold mutable objects. The tuple below can't be changed to refer to different objects, but the list inside it can change:

Numbers

Integers

int has no fixed size: an integer can be as large as your memory allows, so there is no overflow. Underscores make long literals readable, and prefixes select a base:

There is one limit, added in Python 3.11 to stop denial of service attacks: converting an integer with more than 4,300 decimal digits to or from a string raises ValueError. Arithmetic on huge numbers is fine, and power of two bases such as hexadecimal aren't limited. If you really need longer conversions, raise the limit with sys.set_int_max_str_digits():

Floating point numbers

float is a 64 bit binary floating point number (IEEE 754 double precision), good for about 15 to 17 significant decimal digits. Most decimal fractions, such as 0.1, can't be stored exactly in binary, so tiny errors appear:

This isn't a Python bug; every language that uses binary floating point behaves the same way. Two habits follow:

  • Compare floats with math.isclose(a, b) instead of ==. By default it allows a relative difference of about one part in a billion; pass abs_tol= when comparing with values near zero.
  • Round only when you display a value, for example with an f-string such as f"{total:.2f}" (see Strings and text formatting).

float also has the special values float("inf") and float("nan") ("not a number"). A NaN is not equal to anything, not even itself.

Decimal and Fraction for exact arithmetic

For money, use decimal.Decimal, which stores decimal digits exactly. Always create it from a string or an integer: a float has already lost precision before Decimal sees it. fractions.Fraction stores exact ratios.

quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) rounds to cents the way most people expect. The line Decimal(0.1) shows the exact binary value that the float 0.1 really holds.

Tip. Another common approach is to store money as an integer number of cents and only format it as pounds or euros for display.

Complex numbers

Python has complex numbers built in, with j as the imaginary unit: for z = 3 + 4j, z.real is 3.0, z.imag is 4.0 and abs(z) is 5.0. The cmath module has complex versions of the functions in math. You won't need complex numbers again in this course.

Booleans and None

bool

bool has exactly two objects, True and False. It is a subclass of int, with True == 1 and False == 0, which is occasionally handy:

Truthiness

Any object can be used where Python expects a condition, for example in an if. Each object is then treated as truthy or falsy, and bool(obj) tells you which:

FalsyTruthy
False, NoneTrue
zero: 0, 0.0, 0j, Decimal(0), Fraction(0)any nonzero number, including -1
empty containers: "", b"", [], (), {}, set(), range(0)any nonempty container, including "0", " " and [0]

So if items: is the idiomatic way to ask "is this list nonempty?". Note that {} is an empty dict; an empty set is written set().

Pitfall. bool("False") is True: it's a nonempty string. To turn user text into a boolean, compare it explicitly, for example text.strip().lower() in {"yes", "y", "true"}.

None

None is the single object of type NoneType. It means "no value here": a result that doesn't exist yet, an optional setting that wasn't given. A function that ends without a return statement returns None, and so do methods that change an object in place:

The second example is a classic bug: numbers = numbers.sort() throws your list away and keeps None. Use sorted(numbers) when you want a new list.

Test for None with is None or is not None. Don't write if not value: when 0 or "" are valid values, because they are falsy too.

Converting between types

Calling a type converts a value to that type, when a conversion makes sense:

CallResultNotes
int("42"), int(" 42\n")42surrounding whitespace is ignored
int("1_000")1000underscores between digits are allowed
int("ff", 16), int("0b101", 0)255, 5base 0 reads the prefix: 0b, 0o or 0x
int(4.9), int(-4.9)4, -4truncates towards zero; use round() or math.floor() otherwise
int("4.2")ValueErrorparse with float() first if you expect decimals
float("2.5e3")2500.0also accepts "inf" and "nan"
str(3.0), str(None)'3.0', 'None'every object can be turned into a string
bool("False")Truenonempty string; see the pitfall above
list("abc")['a', 'b', 'c']any iterable can feed list, tuple or set
tuple([1, 2]), set([1, 1, 2])(1, 2), {1, 2}set drops duplicates
dict([("a", 1), ("b", 2)]){'a': 1, 'b': 2}from key and value pairs
bytes(5)b'\x00\x00\x00\x00\x00'an integer means "this many zero bytes"
"héllo".encode("utf-8")b'h\xc3\xa9llo'text to bytes; covered in Text, bytes and Unicode

Text typed by a user or read from a file is always a str, so converting it is a daily task. Invalid text raises ValueError, which you can catch with try/except; the Errors and exceptions unit covers this properly:

Naming things

The rules

A name must:

  • start with a letter or an underscore, followed by letters, digits or underscores. "Letter" includes non English letters (PEP 3131), so größe is a valid name, although most projects stick to ASCII;
  • not be a keyword, a word reserved by the language.

Names are case sensitive: total, Total and TOTAL are three different names. The keyword module lists the reserved words:

Soft keywords are special only in certain positions, so they are still valid names: match = 3 works. That output comes from 3.15, which added lazy for explicit lazy imports (PEP 810); 3.13 and 3.14 list the other four.

Conventions (PEP 8)

PEP 8, the style guide for Python code, sets these naming conventions:

Kind of nameStyleExamples
variables, functions, methods, modulessnake_casetotal_price, load_config, file_utils
constantsUPPER_SNAKE_CASEMAX_RETRIES, DEFAULT_TIMEOUT
classes, exceptions, type aliasesCapWordsShoppingCart, ConfigError
internal ("please don't use from outside")leading underscore_cache, _parse_line
a value you deliberately ignore_for _ in range(3):

Choose names that say what a value means: elapsed_seconds rather than t, customers rather than data2. Avoid l, O and I as single letter names; they look like digits in many fonts.

Python has no true constants. MAX_RETRIES = 3 is a normal name; the capitals tell readers not to rebind it. Type checkers can enforce that if you annotate it with typing.Final.

Pitfall. Don't reuse the names of built in functions and types such as list, dict, str, id, type, input, sum or max for your own values. After list = [1, 2], a later list("abc") fails with TypeError: 'list' object is not callable. Use items, names or totals instead.

Assignment forms

Python has several forms of assignment. All of them bind names; none of them copy objects.

  • On the right, 10, 20 builds a tuple; on the left, the names receive its items in order. That's why the swap works: the right side is evaluated completely before any name is rebound.
  • In unpacking, one target may carry a *. It collects all the items not taken by the other targets, always into a list, even when the source is a tuple or a string.
  • The number of targets must match the number of items. Otherwise you get ValueError: too many values to unpack (expected 2, got 3) or not enough values to unpack. Python 3.13 leaves out the got 3 part.

Chained assignment a = b = value binds both names to the same object. That is fine for immutable values, and a trap for mutable ones:

del name removes a name, not an object. The object lives on while other names (or containers) still refer to it; Python frees it automatically once nothing does:

Augmented assignment such as count += 1 is covered in Operators and expressions, including the important detail that += changes a list in place.

Aliasing and copying

When two names refer to the same mutable object, they are aliases. Aliasing is often what you want, for example when a function changes a list you pass to it. When it isn't, make a copy, and choose the right depth:

  • A shallow copy creates a new outer container but puts the same inner objects in it. items.copy(), items[:], list(items), dict(mapping) and copy.copy(obj) all make shallow copies.
  • A deep copy, made with copy.deepcopy(obj), copies the inner objects too, recursively.
Rendering diagram…

The shallow copy didn't get [5, 6], because it has its own outer list. It did see the 99, because it shares inner list A with grid. The deep copy shares nothing.

Pitfall. [[0] * 3] * 3 looks like a 3×3 grid, but it is one row repeated three times: * copies references, not objects. After board[0][0] = 1, every row starts with 1. Build independent rows with a loop or a comprehension: [[0] * 3 for _ in range(3)] (see Collections and comprehensions).

Hashability

A hash is an integer computed from an object's value, which lets dictionaries and sets find items quickly. An object is hashable if its hash never changes during its lifetime and equal objects have equal hashes. Only hashable objects can be dictionary keys or set elements.

The immutable built in types are hashable; the mutable containers are not, because changing them would change their hash. A tuple is hashable only if everything inside it is:

The first error message has this wording since Python 3.14. On 3.13 both lines read unhashable type: 'list'.

Because equal objects must have equal hashes, 1, 1.0 and True count as the same dictionary key: {1: "a", True: "b"} ends up as {1: 'b'}.

Note. The hash of a string is randomised each time Python starts, to make certain attacks harder. Never store hashes or rely on their values between runs; this is also why the display order of a set of strings can change from run to run.

Variable annotations: a preview

You can annotate a name with the type you intend it to hold. This is the same type hint syntax you saw in the Introduction to Python unit:

list[str] means "a list of strings", and float | None means "a float or None". Python doesn't enforce any of this, but a type checker such as mypy or pyright would flag count = "zero". Annotations are most useful on functions, which the Functions unit covers.

Check yourself

  1. What are the three properties every object has, and which of them can change?
  2. After a = [1] and b = a, how many lists exist? What does b.append(2) do to a?
  3. When should you use is instead of ==?
  4. Why can int("7") is 7 be True while int("1000000") is 10 ** 6 is False?
  5. A tuple is immutable. Why can pair = (1, [2]) still change, and is pair hashable?
  6. Why is 0.1 + 0.2 == 0.3 false, and what should you use instead for comparisons and for money?
  7. What does first, *rest = "abc" bind, and what type is rest?
  8. What is the difference between copy.copy(grid) and copy.deepcopy(grid) for a list of lists?

Suggested answers

  1. Identity, type and value. Identity and type never change; the value can change only if the type is mutable.
  2. One list with two names. a sees the change: both print [1, 2].
  3. When you check for a singleton, above all x is None or x is not None. Compare values with ==.
  4. CPython caches small integers, so every 7 is the same object; large integers created at run time are separate objects. The result depends on implementation details, which is why is mustn't be used for values.
  5. The tuple can't be changed to refer to other objects, but the list inside it is mutable and can change. Because it contains a list, hash(pair) raises TypeError: it isn't hashable.
  6. 0.1, 0.2 and 0.3 can't be stored exactly in binary floating point, so the sum is 0.30000000000000004. Compare floats with math.isclose; use Decimal created from strings (or integer cents) for money.
  7. first is 'a' and rest is ['b', 'c']. A starred target always produces a list.
  8. Both create a new outer list. The shallow copy shares the inner lists with grid, so changes to them show up in both; the deep copy copies the inner lists too and shares nothing.

Exercises

These exercises are optional practice. Pick the ones that help you.

Some solutions use try and except, which Errors and exceptions explains in full.

  1. Same or equal? Write relation(a, b) that returns "same object" if a and b are the same object, "equal" if they are different but equal objects, and "different" otherwise. For x = [1]: relation(x, x) → "same object", relation(x, [1]) → "equal", relation(x, [2]) → "different".
  2. Hashable or not? Write is_hashable(value) that returns True if hash(value) succeeds and False if it raises TypeError. Check it with 42, "text", (1, 2), (1, [2]), [1], {} and frozenset({1}).
  3. Fair bill split. Write split_bill(total, people) where total is a Decimal such as Decimal("100.00"). Return a list of people shares in whole cents that differ by at most one cent and add up exactly to total, with the larger shares first. split_bill(Decimal("100.00"), 3) → [Decimal('33.34'), Decimal('33.33'), Decimal('33.33')]. Raise ValueError if people is less than 1.
  4. Parse or default. Write to_int(text, default=None) that returns int(text) when the text is a valid integer (surrounding spaces allowed) and default otherwise. to_int(" 12 ") → 12, to_int("12.5") → None, to_int("", 0) → 0.
  5. Rotate with unpacking. Write rotate_left(values) that returns a tuple with the first item moved to the end, using starred unpacking. rotate_left((1, 2, 3)) → (2, 3, 1), rotate_left("ab") → ('b', 'a'), and an empty input gives ().

Solutions

1. Same or equal?

The identity check comes first: the same object is always equal to itself (apart from the odd case of NaN), so testing == first would hide it.

2. Hashable or not?

3. Fair bill split.

Working in whole cents avoids any rounding: the leftover cents from divmod go to the first people, one each.

4. Parse or default.

int() already ignores surrounding whitespace, so no strip() is needed.

5. Rotate with unpacking.

(*rest, first) unpacks the list into a new tuple. Without the emptiness check, first, *rest = () would raise ValueError because there is nothing to bind to first.