Variables and Types

Easy35 min read

Dynamic Typing

Why Strong + Dynamic Typing Matters

The Problem: Static languages force you to declare types up front, slowing prototyping. Untyped languages let bugs hide until runtime.

The Solution: Python is dynamically typed but strongly typed at runtime — fast to write, predictable when it runs — and optional type hints add static checking on top.

Real Impact: You get the iteration speed of a scripting language with the safety net of a compiler when you want it.

Real-World Analogy

Think of Python types as luggage tags on packages:

  • Value = the package itself — has a definite type at runtime
  • Variable name = the tag — points to whatever package you last attached it to
  • Type hints = the address label — guides the type checker but doesn't constrain the box
  • mypy / pyright = the inspector who checks your labels match reality before shipping

Python is dynamically typed: variables don't have types — values do. A variable is just a name bound to an object. The same name can be re-bound to a different type at any time. Compare this to Go, where each variable has a fixed type known at compile time.

x = 42            # x is bound to an int
x = "hello"       # now bound to a str — perfectly legal
x = [1, 2, 3]      # now a list

print(type(x))   # <class 'list'>

Strongly Typed at Runtime

Python is dynamically typed but strongly typed: "5" + 5 raises TypeError at runtime — Python won't silently coerce types like JavaScript does.

Variables and Naming

Assignment in Python is just binding a name to an object. Multiple names can refer to the same object, and a name has no inherent type.

age = 30
name = "Alice"
is_admin = True

# Multiple assignment
a, b, c = 1, 2, 3
x = y = z = 0          # all three point to the same int 0

# Swap without a temp
a, b = b, a

Naming Rules & Conventions

KindConventionExample
Variable / functionsnake_caseuser_name, compute_total()
ClassPascalCaseUserAccount
ConstantUPPER_SNAKE_CASEMAX_RETRIES = 3
Module / packagelower_caseuser_service.py
“Private”_leading_underscore_internal_state
Name mangling (class)__double_underscore__private (becomes _ClassName__private)
Dunder (magic)__name____init__, __repr__ — reserved for Python

⚠️ Python has no true constants

MAX_RETRIES = 3 is just a regular variable by convention. Tools like mypy can enforce immutability with Final[int].

The Built-in Types

Numbers

# int — arbitrary precision, no overflow
big = 2 ** 100            # 1267650600228229401496703205376

# float — 64-bit IEEE 754 double
pi = 3.14159
scientific = 1.5e-3     # 0.0015

# complex — built into the language
z = 2 + 3j
print(z.real, z.imag)  # 2.0 3.0

# Integer literals: bin / oct / hex / underscores
b = 0b1010              # 10
o = 0o17                # 15
h = 0xff                # 255
big = 1_000_000_000     # underscores allowed for readability

⚠️ Float comparison

0.1 + 0.2 == 0.3 is False. Use math.isclose(a, b) or the decimal / fractions modules for exact arithmetic.

Strings

greeting = "hello"
multi = """Multi-line
string with newlines."""

# f-strings (Python 3.6+) — the modern way to format
name = "Alice"
age = 30
msg = f"{name} is {age} years old"

# Expressions and format specs inside f-strings
pi = 3.14159
print(f"{pi:.2f}")        # '3.14'
print(f"{2**10:_}")        # '1_024'
print(f"{name=}")          # name='Alice' (3.8+ debug syntax)

# Raw strings — no escape processing
path = r"C:\Users\Alice"

# Bytes — raw binary data, distinct from str
data = b"\x89PNG\r\n"

Booleans and None

# bool is a subclass of int. True == 1, False == 0.
flag = True
print(True + True)       # 2

# None — the singleton sentinel value (like null)
result = None

# Always test for None with `is`, never with `==`
if result is None:
    ...

Collections (preview)

numbers = [1, 2, 3]              # list — mutable ordered
coords = (10, 20)              # tuple — immutable ordered
unique = {1, 2, 3}              # set — unordered, no dupes
user = {"name": "Alice"}        # dict — key/value mapping

Each gets its own dedicated tutorial later in the curriculum.

Type Conversion

Convert between types explicitly with built-in constructors. Python won't auto-convert: "5" + 5 raises TypeError.

int("42")         # 42
int(3.7)          # 3 — truncates toward zero
int("ff", 16)     # 255 — second arg is the base

float("3.14")     # 3.14
float("inf")      # inf

str(42)           # '42'
str([1, 2])       # '[1, 2]'

bool(0)           # False — falsy values: 0, '', [], {}, None
bool("hi")        # True

list("abc")       # ['a', 'b', 'c']
tuple([1, 2])     # (1, 2)
set("hello")     # {'h', 'e', 'l', 'o'}

Falsy Values

Python has well-defined truthiness rules. These all evaluate to False in a boolean context:

  • False, None
  • Numeric zero: 0, 0.0, 0j
  • Empty containers: "", [], (), {}, set()
  • Custom classes that define __bool__ returning False or __len__ returning 0

Everything else is truthy. Prefer if items: over if len(items) > 0:.

Identity vs Equality

Two different comparisons confuse beginners coming from other languages. Use == for value equality, is for object identity (same memory location).

a = [1, 2, 3]
b = [1, 2, 3]
c = a

a == b      # True — same contents
a is b      # False — different list objects
a is c      # True — c is just another name for a

id(a), id(b)  # different addresses

Use is for None, True, False — and nothing else

These are singletons: there is exactly one None in a Python process. x is None is canonical. For everything else, == is what you want.

Type Hints (PEP 484)

Since Python 3.5, you can annotate variables, parameters, and return types. These are hints — Python ignores them at runtime, but tools like mypy, pyright, and editors like VS Code use them to catch bugs before you run your code.

Function Signatures

def greet(name: str, age: int = 0) -> str:
    return f"Hello, {name} (age {age})"

def save(user_id: int, data: dict) -> None:
    ...

Variable Annotations

count: int = 0
name: str
name = "Alice"          # annotation alone is allowed

Built-in Generics (3.9+)

You can use the built-in collection types as generic types directly — no need to from typing import List.

def total(prices: list[float]) -> float:
    return sum(prices)

def lookup(users: dict[int, str], uid: int) -> str | None:
    return users.get(uid)

The typing Module

from typing import Optional, Any, Callable, Final

MAX_RETRIES: Final[int] = 3      # cannot be reassigned (checked by mypy)

def parse(raw: str) -> Optional[int]:   # equivalent to: int | None
    ...

def run(fn: Callable[[int], str], n: int) -> str:
    return fn(n)

Running mypy

(.venv) $ pip install mypy
(.venv) $ mypy main.py
Success: no issues found in 1 source file

Type hints are optional

You can adopt them gradually — annotate the bits where types matter most (public APIs, complex functions) and skip the rest. mypy can be configured to be as strict or as lenient as you want.

Common Operations and Built-ins

Built-inPurposeExample
type(x)Class of an objecttype(42)<class 'int'>
isinstance(x, T)Is x of type T (or subclass)isinstance(42, int) → True
id(x)Object identity (CPython: memory address)id(obj)
len(x)Length of a containerlen([1,2,3]) → 3
repr(x)Developer-facing stringrepr("hi")"'hi'"
str(x)User-facing stringstr(3.14)'3.14'
round(x, n)Round to n digits (banker's rounding)round(2.675, 2) → 2.67
abs(x)Absolute valueabs(-5) → 5
min, maxMin / maxmax([3,1,2]) → 3
sumSum of an iterablesum(range(10)) → 45

🎯 Practice Exercises

Exercise 1: Type detective

Write a function describe(x) that prints the type, repr, length (if applicable), and truthiness of any value passed to it.

Exercise 2: Safe int parser

Write parse_int(s: str) -> int | None that returns the integer value of s or None if it can't be parsed. Don't use a bare except.

Exercise 3: Annotate this

Take a small untyped function from any project and add type hints. Run mypy --strict on it. Fix every warning.

Exercise 4: Constant enforcement

Define a configuration module with several Final-typed constants. Try to reassign one — mypy should flag it even though Python at runtime allows it.

Common Pitfalls

⚠️ Avoid These

  • Integer division surprise: 5 / 2 is 2.5 (true division). Use 5 // 2 for integer division (2).
  • x is 1000: CPython caches small ints (-5 to 256). 1 is 1 is True, 1000 is 1000 may be False. Always use == for value comparison.
  • Shadowing built-ins: list = [1,2,3] shadows the built-in list constructor for the rest of the scope. Don't reuse names like list, dict, str, id, type.
  • Type-hint runtime cost: Generic syntax like list[int] evaluates a real object at runtime. In hot paths, put hints in a from __future__ import annotations block to defer evaluation.
  • Confusing str and bytes: Files opened in "rb" mode return bytes. You can't concatenate str and bytes — decode/encode explicitly.