Codetail

Article 1 of 15

Variables & Types

What is data? Why does type matter?

20 min read

Names, not boxes

Most programming tutorials describe variables as "boxes that store values." That mental model works for a while, then breaks in confusing ways. Python's model is different.

In Python, a variable is a name bound to an object. Think of it as a sticky note you put on an object, not a box you pour a value into. The object lives somewhere in memory. The variable is just a label pointing to it.

Python
1x = 42

This line does two things: creates an integer object with value 42, then binds the namexto it. The object exists independently. The name is just how you refer to it.

A variable is a sticky note on an object. Multiple sticky notes can point to the same object. Moving a sticky note doesn't change the object.

Where this matters: aliasing

When you assign one variable to another, you get two names pointing to the same object, not two copies. This is called aliasing, and it surprises almost every new Python programmer at some point.

Python
1a = [1, 2, 3]
2b = a # b points to the SAME list, not a copy
3
4b.append(4)
5print(a) # [1, 2, 3, 4] - a sees the change
6print(b) # [1, 2, 3, 4]
7print(a is b) # True - same object

Both names point to the same list, so a change through either name is visible from both. If you want a copy, you have to ask for one explicitly.

Python
1a = [1, 2, 3]
2b = a.copy() # a real copy, new object
3
4b.append(4)
5print(a) # [1, 2, 3] - unchanged
6print(b) # [1, 2, 3, 4]
7print(a is b) # False - different objects

Why integers feel different

Aliasing seems alarming until you notice that integers and strings don't have this problem. That's because they're immutable: you can't change them in place, so there's no way to affect one name through another.

Python
1a = 10
2b = a
3
4b = b + 1 # creates a NEW integer object
5print(a) # 10 - unchanged
6print(b) # 11 - b now points to a different object

b = b + 1 doesn't modify the integer 10. It creates a new integer 11 and moves the sticky note. The object a still points to is untouched.

The five fundamental types

Python has five types you'll use in nearly every program. Everything else is built on top of them.

int: whole numbers

Python integers are exact and have no size limit. Unlike most languages, you can't overflow an int in Python. It grows to fit whatever you need.

Python
1age = 30
2count = -7
3big = 1_000_000_000 # underscores for readability, same as 1000000000
4
5# No overflow: Python handles arbitrarily large integers
6factorial_20 = 2432902008176640000
7print(factorial_20 * 100)

float: decimal numbers

Floats represent numbers with decimal points using IEEE 754 double precision. They're fast and cover an enormous range, but they can't represent every decimal exactly.

Python
1price = 9.99
2ratio = 0.5
3scientific = 1.5e10 # 15000000000.0
4
5# The classic float gotcha
6print(0.1 + 0.2) # not 0.3
7print(0.1 + 0.2 == 0.3) # False!

0.1 + 0.2 != 0.3 because floats are stored in binary. 0.1 in binary is a repeating fraction, like 1/3 in decimal. The full story is in the Numbers article. For money, use decimal.Decimal.

str: text

An immutable sequence of Unicode characters. Covered in depth in the Strings article.

Python
1name = "Alice"
2message = 'Hello, world!'
3multi = """Line one
4Line two"""
5
6print(type(name)) # <class 'str'>

bool: true or false

Booleans represent truth values. There are exactly two: True and False. The capitalization matters: lowercase versions are just undefined names.

Python
1is_active = True
2has_error = False
3
4# Surprising: bool is a subclass of int
5print(True + True) # 2
6print(True * 5) # 5
7print(int(True)) # 1
8print(int(False)) # 0

True and False are literally just 1 and 0 with extra behavior. This is occasionally useful for counting: summing a list of booleans gives you the count of True values.

None: the absence of a value

Noneis Python's way of saying "no value here." It's not zero, not an empty string, not False. It's the explicit absence of a value. There is exactly one None object in all of Python.

Python
1result = None # no value yet
2
3# Functions that don't return a value implicitly return None
4def say_hello():
5 print("Hello")
6
7x = say_hello()
8print(x) # None
9print(x is None) # True - use "is", not "=="
10
11# Common pattern: optional parameter
12def connect(host, port=None):
13 if port is None:
14 port = 443

Dynamic typing

In languages like Java or C++, you declare the type of a variable upfront and it can never hold anything else. Python works differently: the type lives with the object, not the variable. A name can point to any type of object at any time.

Python
1x = 42
2print(type(x)) # <class 'int'>
3
4x = "hello"
5print(type(x)) # <class 'str'>
6
7x = [1, 2, 3]
8print(type(x)) # <class 'list'>
9
10# x didn't change type - x just points to a different object each time

Python is dynamically typed, not untyped. Objects always have a type. Variables don't. The distinction is important.

Type annotations: hints, not rules

Python 3.5+ lets you annotate variables and function signatures with types. This is purely for documentation and tooling. Python itself ignores annotations at runtime. They don't enforce anything. Tools like mypy and your IDE use them to catch type errors before you run the code.

Python
1# Annotations add clarity and enable editor autocomplete
2name: str = "Alice"
3age: int = 30
4scores: list[int] = [95, 87, 91]
5
6# Function annotations
7def greet(name: str, times: int = 1) -> str:
8 return (f"Hello, {name}! " * times).strip()
9
10print(greet("Alice", 2))
Python
1# Annotations don't stop you from doing the "wrong" thing at runtime
2def double(n: int) -> int:
3 return n * 2
4
5print(double(5)) # 10 - works as intended
6print(double("ha")) # "haha" - Python doesn't enforce the annotation
7print(double([1, 2])) # [1, 2, 1, 2] - same

In practice: annotate your functions and class attributes. Skip annotations on local variables inside functions unless the type is non-obvious. Run mypy or use an IDE that checks types as you write. The earlier you catch type errors, the cheaper they are to fix.

Everything is an object

In Python, every value is an object: numbers, strings, functions, classes, modules. This isn't just philosophy: it means every value in Python has three things attached to it.

Identity

id(x)

A unique integer identifying this object. Think of it as its memory address. Guaranteed unique for the lifetime of the object.

Type

type(x)

What kind of object it is. The type determines which operations are valid and what the data means.

Value

x

The actual data. For an integer it's the number. For a string it's the characters. For a list it's the elements.

Python
1x = 42
2
3print(id(x)) # memory address, e.g. 9788576
4print(type(x)) # <class 'int'>
5print(x) # 42
6
7# Works for every object, even functions
8def greet(): pass
9
10print(type(greet)) # <class 'function'>
11print(id(greet)) # functions have identities too

Identity vs equality

Python gives you two ways to compare things. They test different questions.

a == b

Do they have the same value? Two separate lists with the same contents are equal.

a is b

Are they the same object? Same identity (id). Like asking whether two people have the same name vs are literally the same person.

Python
1a = [1, 2, 3]
2b = [1, 2, 3] # same values, different object
3c = a # same object
4
5print(a == b) # True - same value
6print(a is b) # False - different objects
7
8print(a == c) # True - same value
9print(a is c) # True - same object

Rule: use == to compare values. Use is only when you specifically mean "same object", most commonly x is None. Using is to compare integers or strings can silently give wrong answers due to interning.

Checking and converting types

Checking types

You have two tools: type() returns the exact type. isinstance() checks whether an object is an instance of a type or any of its subclasses. Useisinstance()almost everywhere.

Python
1x = True
2
3# type() checks the exact type
4print(type(x) == bool) # True
5print(type(x) == int) # False - even though True is stored as 1
6
7# isinstance() checks the type and its parents
8print(isinstance(x, bool)) # True
9print(isinstance(x, int)) # Also True - bool is a subclass of int
10
11# isinstance() handles multiple types at once
12def process(value):
13 if isinstance(value, (int, float)):
14 return value * 2
15 if isinstance(value, str):
16 return value.upper()
17
18print(process(5)) # 10
19print(process("hi")) # "HI"

Converting types

Python won't silently convert types for you. When you need a different type, you ask for it explicitly.

Python
1# str → int
2int("42") # 42
3int(" 10 ") # 10 - strips whitespace
4
5# str → float
6float("3.14") # 3.14
7
8# number → str
9str(42) # "42"
10str(3.14) # "3.14"
11
12# int ↔ float
13int(3.9) # 3 - truncates toward zero, does NOT round
14float(7) # 7.0
15
16# anything → bool
17bool(1) # True
18bool(0) # False
19bool("hello") # True
20bool("") # False - empty string is falsy
21bool([1, 2]) # True
22bool([]) # False - empty list is falsy

Gotcha: int(3.9) is 3, not 4. It truncates toward zero, not rounds. Use round(3.9) if you want rounding.

Conversions that fail

Not every conversion makes sense. Python raises an exception rather than silently producing a wrong result.

Python
1int("hello") # ValueError: invalid literal for int()
2int("3.14") # ValueError - use float() first, then int()
3int(None) # TypeError: int() argument must be a string or number
4
5# The safe pattern: try/except
6def safe_int(value):
7 try:
8 return int(value)
9 except (ValueError, TypeError):
10 return None
11
12print(safe_int("42")) # 42
13print(safe_int("hello")) # None
14print(safe_int(None)) # None

Truthiness: what counts as True

Every Python object has a boolean interpretation. You don't have to write if len(items) > 0. Just write if items. The rule is simple: empty and zero-like values are falsy, everything else is truthy.

Falsy values

Falsethe boolean False
Noneno value
0zero integer
0.0zero float
""empty string
[]empty list
{}empty dict
set()empty set

Truthy values

Truethe boolean True
1any non-zero int
"hello"any non-empty string
[0]list with items (even if 0)
{'a': 1}dict with items
{0}set with items
object()any object instance
Python
1items = []
2if items:
3 print("has items")
4else:
5 print("empty") # this runs
6
7name = " "
8if name.strip():
9 print("has name")
10else:
11 print("blank name") # this runs - " ".strip() is ""
12
13# Common pattern: default value
14user_input = ""
15display = user_input or "Anonymous"
16print(display) # "Anonymous"

Assignment patterns

Python has several assignment forms beyond the basic x = value. Each one has a specific use case.

Augmented assignment

A shorthand for updating a variable in terms of its current value.

Python
1count = 0
2count += 1 # same as count = count + 1
3count += 1
4print(count) # 2
5
6total = 100
7total -= 20 # subtract
8total *= 1.1 # multiply (apply 10% markup)
9print(total) # 88.0
10
11text = "hello"
12text += " world" # works on strings too
13print(text)

Multiple assignment

Assign the same value to several names in one line. Note that this binds all names to the same object, fine for immutables but potentially confusing for mutables.

Python
1x = y = z = 0 # all three names point to the same 0
2print(x, y, z) # 0 0 0
3
4# Fine for immutables, 0 can't be mutated
5x += 1
6print(x, y, z) # 1 0 0 - x now points to a different object
7
8# Be careful with mutables
9a = b = [] # both point to the SAME list
10a.append(1)
11print(a, b) # [1] [1] - surprise!

Tuple unpacking

Assign multiple values in one line by unpacking from a tuple, list, or any iterable. The number of names must match the number of values. Use a star to capture the rest.

Python
1# Basic unpacking
2x, y = 10, 20
3print(x, y) # 10 20
4
5# Swap without a temp variable
6x, y = y, x
7print(x, y) # 20 10
8
9# Unpack a list
10first, second, third = [1, 2, 3]
11
12# Star captures the remainder
13head, *tail = [1, 2, 3, 4, 5]
14print(head) # 1
15print(tail) # [2, 3, 4, 5]
16
17first, *middle, last = [1, 2, 3, 4, 5]
18print(first, last) # 1 5
19print(middle) # [2, 3, 4]

Walrus operator (Python 3.8+)

The := operator assigns a value and returns it at the same time. Useful when you want to test a value and use it in the same expression.

Python
1import re
2
3text = "Order #12345 placed"
4
5# Without walrus: runs the regex twice
6if re.search(r"\d+", text):
7 match = re.search(r"\d+", text)
8 print(match.group())
9
10# With walrus: assign and test in one step
11if match := re.search(r"\d+", text):
12 print(match.group()) # "12345"
13
14# Also useful in while loops
15data = [3, 7, 1, 9, 2]
16while (n := data.pop()) > 2:
17 print(n) # 2 < threshold, stops after 9