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
| Kind | Convention | Example |
|---|---|---|
| Variable / function | snake_case | user_name, compute_total() |
| Class | PascalCase | UserAccount |
| Constant | UPPER_SNAKE_CASE | MAX_RETRIES = 3 |
| Module / package | lower_case | user_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__returningFalseor__len__returning0
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-in | Purpose | Example |
|---|---|---|
type(x) | Class of an object | type(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 container | len([1,2,3]) → 3 |
repr(x) | Developer-facing string | repr("hi") → "'hi'" |
str(x) | User-facing string | str(3.14) → '3.14' |
round(x, n) | Round to n digits (banker's rounding) | round(2.675, 2) → 2.67 |
abs(x) | Absolute value | abs(-5) → 5 |
min, max | Min / max | max([3,1,2]) → 3 |
sum | Sum of an iterable | sum(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 / 2is2.5(true division). Use5 // 2for integer division (2). x is 1000: CPython caches small ints (-5 to 256).1 is 1is True,1000 is 1000may be False. Always use==for value comparison.- Shadowing built-ins:
list = [1,2,3]shadows the built-inlistconstructor for the rest of the scope. Don't reuse names likelist,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 afrom __future__ import annotationsblock to defer evaluation. - Confusing
strandbytes: Files opened in"rb"mode return bytes. You can't concatenatestrandbytes— decode/encode explicitly.