Python Essentials for AI Engineers · Lesson 3 of 36
What is dynamic typing?
Static vs Dynamic Typing
In a statically typed language (Java, C++, Go), the type of a variable is declared at compile time and cannot change:
// Java — type is fixed at declaration
int count = 42;
count = "hello"; // Compile errorIn Python, types are dynamic — a variable is just a name that points to an object. The name can be rebound to any object:
# Python — name can point to any object
count = 42
print(type(count)) # <class 'int'>
count = "hello" # No error — count now points to a string
print(type(count)) # <class 'str'>
count = [1, 2, 3] # Now it's a list
print(type(count)) # <class 'list'>The variable count isn't typed — the object 42 is typed as int. The variable is just a label.
Variables are References
This has a critical implication: assignment copies the reference, not the object.
a = [1, 2, 3]
b = a # b points to the SAME list as a
b.append(4)
print(a) # [1, 2, 3, 4] — a changed because a and b point to the same object
# To copy, use .copy() or list()
b = a.copy()
b.append(5)
print(a) # [1, 2, 3, 4] — unchanged
print(b) # [1, 2, 3, 4, 5]
# Deep copy for nested structures
import copy
nested = [[1, 2], [3, 4]]
shallow = nested.copy() # Copies the outer list, not inner lists
deep = copy.deepcopy(nested) # Copies everything recursivelyThis matters in AI code when passing tensors, DataFrames, or lists between functions — mutation can cause bugs that are hard to trace.
type() and isinstance()
x = 42
print(type(x)) # <class 'int'>
print(type(x) == int) # True — works but not idiomatic
# isinstance(): preferred — handles inheritance correctly
print(isinstance(x, int)) # True
print(isinstance(x, float)) # False
print(isinstance(x, (int, float))) # True — check against multiple types
# Inheritance example
class Animal:
pass
class Dog(Animal):
pass
dog = Dog()
print(type(dog) == Dog) # True
print(type(dog) == Animal) # False — type() does NOT check inheritance
print(isinstance(dog, Dog)) # True
print(isinstance(dog, Animal)) # True — isinstance() checks inheritanceRule: Always use isinstance() for type checking. Use type() only when you need the exact class.
Duck Typing
Python uses duck typing: if an object has the right methods/attributes, it works — regardless of its class.
"If it walks like a duck and quacks like a duck, it's a duck."
def compute_total_cost(items):
"""Sum the cost of items — works with any iterable of numbers."""
return sum(items) # Works with list, tuple, generator, np.array...
print(compute_total_cost([1.5, 2.3, 0.8])) # 4.6
print(compute_total_cost((1.5, 2.3, 0.8))) # 4.6 — tuple
print(compute_total_cost(x * 0.1 for x in range(5))) # generator
# LangChain uses duck typing extensively:
# Any object with an .invoke() method works as a Runnable
# Any object with a .similarity_search() method works as a vector storeThis is why Python code is often more flexible than Java — you don't need interfaces or abstract base classes to achieve polymorphism.
Type Hints
Dynamic typing doesn't mean no types — it means types are checked at runtime, not compile time. Type hints add documentation and enable static analysis without changing runtime behavior:
# Without type hints — unclear what types are expected
def calculate_dose(drug, weight, egfr):
...
# With type hints — clear contract
def calculate_dose(drug: str, weight: float, egfr: float) -> str:
"""Calculate adjusted dose for renal impairment."""
...
# Type hints are not enforced at runtime
calculate_dose("warfarin", "70kg", 45) # No runtime error — hint is ignoredType Hints in Practice
from typing import Optional, Union
# Optional[X] means X or None
def get_patient_age(patient_id: str) -> Optional[int]:
db = {"P001": 67, "P002": 45}
return db.get(patient_id) # Returns int or None
# Union[X, Y] means X or Y (Python 3.10+: X | Y)
def normalize(value: Union[int, float]) -> float:
return float(value)
# Python 3.10+ syntax (preferred)
def normalize_new(value: int | float) -> float:
return float(value)
# Generic types
from typing import List, Dict, Tuple
def batch_embed(texts: list[str]) -> list[list[float]]:
"""Embed a list of texts, returning a list of embedding vectors."""
...
def build_metadata(docs: list[dict]) -> dict[str, str]:
...
# TypedDict for dict schemas
from typing import TypedDict
class PatientRecord(TypedDict):
patient_id: str
age: int
medications: list[str]
inr: float
def process_patient(record: PatientRecord) -> str:
return f"Patient {record['patient_id']}: INR {record['inr']}"Mypy: Static Analysis for Python
Install mypy to catch type errors before runtime:
# drug_tool.py
def get_dose(drug: str) -> int:
doses = {"warfarin": 5, "metformin": 500}
return doses.get(drug) # BUG: .get() returns int | None, not int
# mypy drug_tool.py:
# error: Incompatible return value type (got "int | None", expected "int")pip install mypy
mypy drug_tool.py # Catches the None case at analysis time, not at runtimeMypy is used by LangChain, FastAPI, Pydantic — the major AI framework libraries all use it.
Where Dynamic Typing Causes Real Bugs
# Bug 1: Silent type coercion
def add(a, b):
return a + b
add(2, 3) # 5 — intended
add("2", "3") # "23" — string concat, not addition — no error!
# Fix: type check at function boundary
def add_ints(a: int, b: int) -> int:
if not isinstance(a, int) or not isinstance(b, int):
raise TypeError(f"Expected int, got {type(a)} and {type(b)}")
return a + b
# Bug 2: Mutable default arguments (classic Python gotcha)
def add_drug(drug: str, drug_list: list = []) -> list: # BUG: [] is created once
drug_list.append(drug)
return drug_list
print(add_drug("warfarin")) # ["warfarin"]
print(add_drug("aspirin")) # ["warfarin", "aspirin"] — shared state!
# Fix: use None as default, create new list inside function
def add_drug_fixed(drug: str, drug_list: list | None = None) -> list:
if drug_list is None:
drug_list = []
drug_list.append(drug)
return drug_list
# Bug 3: None slipping through
def process_score(score: float) -> str:
return f"Score: {score:.2f}"
process_score(None) # TypeError at runtime, not at type-hint time
# Fix: check at boundary or use mypyDynamic Typing Summary
| Aspect | What it means |
|---|---|
| Variables hold references | Assignment doesn't copy data — be careful with mutation |
| Types are on objects | type(x) asks the object, not the variable |
| Duck typing | Works if it has the right methods — no interfaces needed |
| Type hints | Documentation + static analysis tools (mypy) — not enforced at runtime |
| isinstance() preferred | Handles inheritance; type() does not |
| Mutable default args | Classic bug — use None as default, create inside function |