Learnixo

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
// Java — type is fixed at declaration
int count = 42;
count = "hello";  // Compile error

In Python, types are dynamic — a variable is just a name that points to an object. The name can be rebound to any object:

Python
# 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.

Python
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 recursively

This matters in AI code when passing tensors, DataFrames, or lists between functions — mutation can cause bugs that are hard to trace.


type() and isinstance()

Python
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 inheritance

Rule: 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."

Python
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 store

This 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:

Python
# 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 ignored

Type Hints in Practice

Python
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:

Python
# 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")
Bash
pip install mypy
mypy drug_tool.py   # Catches the None case at analysis time, not at runtime

Mypy is used by LangChain, FastAPI, Pydantic — the major AI framework libraries all use it.


Where Dynamic Typing Causes Real Bugs

Python
# 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 mypy

Dynamic 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 |