Production Scenario Interview Questions
Your team migrates a 500K-line codebase to type hints and mypy strict mode. Mypy passes locally but fails in CI with different versions of mypy and Python. Type stubs are out of sync with actual code. How do you manage type checking across environments?
Type checking is environment-dependent: Python version, mypy version, type stub versions, and library versions all affect mypy's behavior. Issues: (1) mypy in CI uses different Python version than local—features like PEP 604 (X | Y) only work in Python 3.10+, (2) type stubs from typeshed may be outdated or incompatible with installed library versions, (3) mypy cache can be stale—different Python versions generate different cache, (4) missing py.typed marker in third-party packages breaks type inference, (5) ignoring errors with # type: ignore loses type safety. Solutions: (1) pin mypy version in CI: `mypy==1.8.0` with specific Python version requirement in pyproject.toml, (2) use `--python-version` flag: `mypy --python-version 3.11 src/`, (3) pre-commit hook with mypy to catch errors before CI, (4) install type stubs explicitly: `types-requests`, `types-flask`, etc., verify stubs are up-to-date, (5) use `--strict` locally and in CI for consistency, (6) enable `disallow_untyped_defs` to force all functions to be typed, (7) check if libraries have py.typed marker: `site-packages/lib/py.typed` (zero-byte file). Debugging: run `mypy --show-traceback --verbose` to see what mypy is checking. Test: ensure CI mypy version, Python version, and type stub versions match local environment. Use tox to test multiple Python versions: `tox -e py{39,310,311,312}-mypy`.
Follow-up: What does the py.typed marker do? How do you enforce that all third-party dependencies have type stubs? What's the difference between --strict and --strict-optional?
Your API accepts JSON payloads and parses them with type hints: `def process(data: dict[str, Any])`. The type hint says dict, mypy is happy, but at runtime, a user sends a JSON array instead and crashes with AttributeError. Type hints didn't catch this. How do you add runtime type validation?
Type hints are static (mypy is a linter)—they don't execute at runtime. At runtime, Python doesn't check type hints by default. Solutions: (1) use pydantic for runtime validation: `from pydantic import BaseModel, ValidationError; class DataModel(BaseModel): key: str; value: int` and `DataModel.model_validate(data)` raises ValidationError on mismatch, (2) use typeguard: `from typeguard import typechecked; @typechecked; def process(data: dict[str, int]):` raises TypeError at runtime if data is wrong type, (3) use beartype for minimal overhead: `from beartype import beartype; @beartype; def process(data: dict[str, int]):`, (4) add explicit isinstance checks: `if not isinstance(data, dict): raise TypeError(...)`, (5) use dataclasses with type hints (not runtime checking) or msgspec for validation. Best practice: use pydantic for API input validation—it catches type errors, validates ranges, coerces types (string to int), and provides good error messages. For performance-critical code, use beartype or typeguard sparingly; for I/O boundaries (API, database, files), always validate. Example: `from pydantic import BaseModel; class APIRequest(BaseModel): query: str; limit: int = 10; APIRequest.model_validate_json(request_body)`. Testing: send invalid JSON to your API and verify validation errors, not AttributeError. Note: type hints + mypy prevent type errors in development; runtime validators prevent type errors in production from external input.
Follow-up: What's the performance overhead of @typechecked? How does pydantic's validation compare to beartype? Can you use both type hints and runtime validation together?
Your codebase has generic types: `class Repository(Generic[T]): def get(self) -> T:`. A subclass forgets to specify T: `class UserRepository(Repository):` and code that uses the repository crashes. Mypy doesn't catch this. Why?
Generic type parameters must be explicitly bound in subclasses. If not specified, T defaults to Any, losing type information. Issues: (1) mypy in default mode doesn't catch unspecified generics—enable `--strict` to catch this, (2) if T isn't bound, type checker assumes T = Any, and code like `user = repo.get(); user.name` doesn't type-check (Any allows any attribute), (3) at runtime, generics are erased—`Repository[User]` and `Repository` are identical at runtime (no T value stored). Solutions: (1) use `--strict` mode which requires explicit generic binding, (2) enforce via type hints: `class UserRepository(Repository[User]):` explicitly binds T, (3) add assert or type guard at runtime: `assert self._type_param is not None` to catch missing bindings, (4) use TypeVar with bounds: `T = TypeVar('T', bound=BaseModel)` to constrain valid types, (5) use __orig_bases__ at runtime to extract generic parameters: `Repository[User].__orig_bases__[0].__args__[0]` gives User. Example correct code: `T = TypeVar('T'); class Repository(Generic[T]): def get(self) -> T: ...; class UserRepository(Repository[User]): ...`. Testing: use isinstance() at runtime with __orig_bases__ inspection to verify generic binding. Debugging: use typing.get_args(cls) and typing.get_origin(cls) to inspect generic parameters at runtime.
Follow-up: How do you extract generic type parameters at runtime? What does __orig_bases__ contain? Can you use typing.get_args() on instances or only on types?
Your team uses TypedDict for API schemas: `class UserSchema(TypedDict): id: int; name: str`. A developer assigns a dict with extra keys and mypy is silent. At runtime, the extra key shadows a function. How do you prevent TypedDict misuse?
TypedDict is structural typing—it only checks required/optional keys, not extra keys or strict mode. Issues: (1) TypedDict doesn't prevent extra keys by default—`{'id': 1, 'name': 'Alice', 'extra': 'data'}` is valid, (2) extra keys can shadow attributes or methods when unpacked with **dict, (3) at runtime, TypedDict is just a regular dict—no enforcement, (4) if a required key is missing but filled later, mypy doesn't catch it. Solutions: (1) use `total=False` to mark all keys optional: `class UserSchema(TypedDict, total=False):` but this is loose, (2) use `Required` and `NotRequired` (PEP 655) for fine-grained control: `class UserSchema(TypedDict): id: Required[int]; extra: NotRequired[str]`, (3) enable `--disallow-untyped-defs` to catch untyped assignments, (4) use pydantic BaseModel instead of TypedDict for validation—pydantic enforces type and extra keys, (5) at runtime, validate with isinstance() and manual key checks, (6) use mypy plugin or assertion to verify extra keys aren't present. Example strict code: `from typing_extensions import TypedDict; class UserSchema(TypedDict): id: int; name: str; # No extra keys allowed`. For validation, convert to pydantic: `class User(BaseModel): id: int; name: str; model_config = ConfigDict(extra='forbid')` forbids extra keys. Testing: mypy with `--strict` catches many TypedDict issues; use runtime validators (pydantic) for external input.
Follow-up: What's the difference between TypedDict and NamedTuple? How do Required and NotRequired work? Can you enforce extra='forbid' in TypedDict?
Your async library mixes callbacks and coroutines. Type hints show `Callable[[int], Awaitable[str]]` but developers pass `Callable[[int], str]` and mypy is silent. At runtime, await fails with TypeError. How do you type-check async callbacks?
Async type hints require Awaitable, but developers often forget the Awaitable wrapper. Issues: (1) `Callable[[int], str]` and `Callable[[int], Awaitable[str]]` are structurally different but mypy may not flag the mismatch if both are valid in some contexts, (2) at runtime, if you do `result = await callback(arg)` and callback returns str (not awaitable), TypeError occurs: "object str can't be used in 'await' expression", (3) protocol-based type checking may not catch this if protocols accept both sync and async. Solutions: (1) use explicit type annotations: `async_callback: Callable[[int], Awaitable[str]]` with `Awaitable` to force async, (2) use Union if both sync and async are valid: `Callable[[int], Union[str, Awaitable[str]]]` and check at runtime with inspect.iscoroutine(), (3) at runtime, verify result is awaitable: `if inspect.iscoroutine(result): result = await result; else: result = result`, (4) use type guards: `@typeguard.typechecked; def async_callback(x: int) -> Awaitable[str]:` to catch mismatches, (5) use asyncio.iscoroutinefunction() to verify callback is async before calling. Example correct code: `async def callback(x: int) -> str: ...; callback_type: Callable[[int], Awaitable[str]] = callback`. Testing: pass sync callbacks to async function and verify mypy error; test runtime with typeguard. Mypy `--strict` with `disallow_untyped_defs` catches most async type issues.
Follow-up: How do you type-hint sync-or-async callbacks? Does inspect.iscoroutine() work on all awaitable objects? Can you use Protocol to define async callback signatures?
Your codebase uses inheritance and protocol-based duck typing. Mypy with `--strict` complains about missing type annotations in overridden methods. A junior dev disables mypy checking with `# type: ignore[override]` on 100+ lines. How do you enforce type consistency in subclasses?
Overridden methods must maintain type consistency (Liskov substitution principle). Issues: (1) if a subclass method has different signature than parent, mypy flags it, but `# type: ignore` hides the error, (2) if parent uses Any or untyped arguments, subclasses inherit that looseness, (3) protocol-based typing requires explicit compliance—if a class claims to implement a protocol, mypy checks all methods match. Solutions: (1) enforce strict typing on parent classes first: annotate all parent methods with specific types, (2) use `--strict` to force all methods to be typed, (3) document override contracts: `@override` decorator (Python 3.12+) signals intent and mypy checks it, (4) use Protocol for explicit contracts: `class Comparable(Protocol): def __lt__(self, other: Comparable) -> bool:` and implement it in subclasses, (5) if suppressing error, add comment explaining why (not just `# type: ignore`). Example correct code: `class Base: def process(self, data: int) -> str: ...; class Child(Base): def process(self, data: int) -> str: ... # mypy enforces signature match`. For legacy code with many `# type: ignore`: create mypy stub files (.pyi) to define correct signatures separately, then refactor implementation to match. Testing: use `mypy --no-ignore-errors` to find all `# type: ignore` uses and address them systematically. Use `@override` decorator in Python 3.12+: `from typing import override; class Child(Base): @override; def process(self, data: int) -> str: ...`.
Follow-up: What's the @override decorator? Does it enforce Liskov substitution? How do stub files (.pyi) interact with mypy checking?
Your team uses mypy plugins for custom type checking (e.g., for ORM query validation). Plugins work locally but fail in CI with different mypy versions. Type checking is inconsistent. How do you manage mypy plugin compatibility?
Mypy plugins are version-specific and fragile. Issues: (1) mypy's internal API changes between versions—plugins targeting mypy 1.8 may fail on 1.9, (2) plugins loaded from pyproject.toml must be importable in CI environment, (3) if plugin raises exception, mypy silently skips it or fails cryptically, (4) typing ecosystem evolves—older plugins may not support new type features (PEP 604, etc.). Solutions: (1) pin mypy version tightly: `mypy==1.8.0` not `mypy>=1.8`, (2) test plugins in CI explicitly: add test step that imports plugin and verifies it loads, (3) monitor mypy changelog for plugin-breaking changes—subscribe to mypy releases, (4) prefer type hints over plugins: if mypy built-ins handle your use case, use them (e.g., Protocol instead of custom plugin), (5) document plugin requirements: which mypy versions, Python versions, dependencies, (6) for custom type checking, use runtime validators (pydantic, typeguard) instead of mypy plugins if possible—simpler and more reliable. Example config: `[tool.mypy] plugins = ["mypy_plugin"]; python_version = "3.11"; warn_unused_ignores = true`. Testing: CI should run `python -c "from mypy_plugin import *"` to verify plugin loads. For complex type validation, prefer pydantic + mypy over custom plugins.
Follow-up: What's the mypy plugin API? How do you write a plugin that's compatible across mypy versions? Can you use pydantic plugins instead of mypy plugins for type validation?
Your data pipeline loads JSON configs with optional nested fields: `{'db': {'host': 'localhost', 'port': 5432}, 'cache': {'ttl': 3600}}`. Some fields are missing in production configs. Type hints and mypy are fine, but at runtime, KeyError or AttributeError crashes. How do you safely handle optional nested dicts?
Type hints don't prevent KeyError at runtime. Issues: (1) if you hint `config: dict[str, dict[str, int]]` but config['cache'] is missing, accessing it raises KeyError, (2) type hints don't enforce structure—they're for static checking only, (3) at runtime, you must check presence before access, (4) using .get() silently returns None, which can cause downstream errors if not handled. Solutions: (1) use pydantic with Optional fields: `class DBConfig(BaseModel): host: str; port: int = 5432; class Config(BaseModel): db: Optional[DBConfig] = None; cache: Optional[dict] = None`, then `Config.model_validate(data)` validates and coerces, (2) use .get() with defaults: `db_host = config.get('db', {}).get('host', 'localhost')` but this is verbose, (3) use dict.get() with type narrowing: `db = config.get('db'); if db: host = db['host']`, (4) use dataclasses_json or msgspec for declarative parsing, (5) at minimum, add try/except with helpful error messages: `try: host = config['db']['host']; except KeyError as e: raise ValueError(f"Missing config key: {e}")`. Example with pydantic: `class Config(BaseModel): db: DBConfig | None = None; model_config = ConfigDict(extra='forbid')` ensures no extra keys and validates all fields. Testing: test with missing, extra, and malformed keys to verify errors are caught and reported clearly, not as cryptic KeyError.
Follow-up: Can you combine mypy with pydantic to catch config errors at both type-check and runtime? How does pydantic's JSON schema generation help with config validation?