Learnixo
Back to blog
AI Systemsintermediate

What is dynamic typing?

Understand Python's dynamic type system: how variables hold references, how type() and isinstance() work, when dynamic typing helps and when it causes bugs, and how type hints add clarity.

Asma Hafeez KhanMay 16, 20266 min read
PythonDynamic TypingType HintsType SafetyDuck Typing
Share:š•

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 |

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.