Type Casting, Type Inspection, and Annotations

Updated

September 7, 2026

Type Casting, Type Inspection, and Annotations

After reading this chapter, you will master Python’s strong typing model, safely perform type conversions, inspect object types at runtime, and write modern Python 3.14 type annotations.

Mental model

Python is strongly typed and dynamically typed: - Strongly typed: Python does not implicitly coerce incompatible types (e.g. adding a string and an integer raises TypeError). - Dynamically typed: Types belong to values, not to variable names. A name can point to an integer at one moment and a string the next.

Strong Typing Enforcement:
  "5" + 2  ──▶ TypeError: can only concatenate str (not "int") to str
Explicit Conversion:
  int("5") + 2  ──▶ 7 (Integer addition)
  "5" + str(2)  ──▶ "52" (String concatenation)

At runtime, isinstance() checks if an object belongs to a class or any of its subclasses, traversing the inheritance tree.


Minimal example

Save as type_inspect_demo.py:

# type_inspect_demo.py
def process_metric(raw_value: str | int | float) -> float:
    # 1. Type inspection with isinstance
    if isinstance(raw_value, (int, float)):
        return float(raw_value)

    # 2. Explicit type casting
    try:
        return float(raw_value)
    except ValueError as err:
        raise ValueError(f"Cannot cast {raw_value!r} to float") from err

def main() -> None:
    samples = [42, "128.5", 3.14159, "invalid_num"]

    for sample in samples:
        try:
            result = process_metric(sample)
            print(f"Sample: {sample!r:15s} -> Processed float: {result}")
        except ValueError as err:
            print(f"Sample: {sample!r:15s} -> Error: {err}")

if __name__ == "__main__":
    main()

Run via uv run python type_inspect_demo.py:

Sample: 42              -> Processed float: 42.0
Sample: '128.5'         -> Processed float: 128.5
Sample: 3.14159         -> Processed float: 3.14159
Sample: 'invalid_num'   -> Error: Cannot cast 'invalid_num' to float

Worked examples

Case 1: Checking types: isinstance vs type()

isinstance() respects class inheritance hierarchies, while type(obj) is Class requires an exact type match.

# isinstance_vs_type.py
class Animal:
    pass

class Dog(Animal):
    pass

def verify_types() -> None:
    d = Dog()

    print(f"type(d) is Dog    : {type(d) is Dog}")
    print(f"type(d) is Animal : {type(d) is Animal} (False: ignores inheritance)")
    print(f"isinstance(d, Animal): {isinstance(d, Animal)} (True: recognizes subclass)")

    # bool is a subclass of int!
    print(f"isinstance(True, int): {isinstance(True, int)} (True!)")
    print(f"type(True) is int    : {type(True) is int} (False!)")

if __name__ == "__main__":
    verify_types()

Run:

uv run python isinstance_vs_type.py

Case 2: Modern Python 3.14 type annotations

Type annotations provide documentation and allow static type checkers like mypy and pyright to find bugs before runtime:

# typed_service.py
def calculate_throughput(requests: int, duration_seconds: float) -> float:
    if duration_seconds <= 0:
        raise ValueError("Duration must be positive")
    return requests / duration_seconds

def lookup_node(node_id: str) -> str | None:
    nodes = {"node-1": "10.0.0.1", "node-2": "10.0.0.2"}
    return nodes.get(node_id)

if __name__ == "__main__":
    tps = calculate_throughput(10_000, 4.5)
    print(f"Throughput: {tps:.2f} req/s")
    print(f"Lookup: {lookup_node('node-1')}")

Run:

uv run python typed_service.py

Pitfalls

Pitfall 1: Using type(x) == T instead of isinstance()

Checking type(x) == int will return False if someone passes a subclass or specialized numerical type. Always prefer isinstance(x, int).

Pitfall 2: Assuming type annotations enforce runtime checks

Python does not validate type hints at runtime by default. If you pass a string to a function annotated with x: int, Python executes it without error until an incompatible operation occurs. Runtime validation requires libraries like Pydantic.


Exercises

  1. Write a function safe_int_cast(val: str, default: int = 0) -> int that parses an integer string and returns default if parsing fails.
  2. Given a mixed list items = [1, "two", 3.0, [4], (5,), {"six": 6}], write a script that separates elements into a dictionary grouped by type name.
  3. Run mypy or ruff check on a script containing a deliberate type annotation mismatch to observe static analysis feedback.

Further reading

  • PEP 484: Type Hints.
  • PEP 604: Allow-writing union types as X | Y.
  • Python Standard Library: typing module.