Right, so you’ve imported a glorious third-party library that does exactly what you need. You’ve dutifully annotated your own code, feeling the warm glow of type safety. You run mypy and… a firestorm of red. The library has no type hints. Your brilliant annotations are now pointing into a void, and your type checker is having a panic attack.

Welcome to the most common real-world hurdle in Python’s type system. You can’t change the library’s source code, but you also refuse to surrender to Any and the ensuing chaos. Let’s fix this.

The Stub File: Your Secret Weapon

Your first and best line of defense is a stub file (.pyi). Think of it as a shadow version of the library’s code that contains only type annotations. The type checker reads your stub instead of the actual, unannotated library code.

You create these in a directory, typically named stubs/, and tell your type checker where to look. The magic is in the file structure: your stub file’s path and name must exactly mirror the library’s module. For a library some_lib with a module.py, you create stubs/some_lib/module.pyi.

# Your project structure
your_project/
├── src/
├── stubs/
   └── some_lib/
       └── module.pyi  # <-- This is your stub
└── pyproject.toml

Inside that .pyi file, you only write signatures—no implementation logic.

# stubs/some_lib/module.pyi
import datetime
from typing import Any, Dict, Iterable

class FancyDatabaseConnector:
    def __init__(self, connection_string: str) -> None: ...
    def get_records(self, query: str, limit: int = 100) -> Iterable[Dict[str, Any]]: ...
    def write_record(self, data: Dict[str, Any], timestamp: datetime.datetime) -> int: ...

See those ellipses (...)? They’re the official syntax for “a function body exists, but we’re not defining it here.” It’s the placeholder that makes the type checker happy.

Configuring Your Type Checker to Find Stubs

Creating the stub is only half the battle; you have to point your type checker at the stubs/ directory. For mypy, you add this to your pyproject.toml:

[tool.mypy]
mypy_path = "stubs"

Pyright, being the fancy VS Code-native tool, often needs a configuration in your pyrightconfig.json:

{
  "stubPath": "stubs",
  "extraPaths": ["stubs"]
}

Without this, you’ve baked a delicious cake and left it in another room. The type checker will never find it and will just assume everything is Any.

The Quick and Dirty Inline Fix: # type: ignore

Sometimes you’re in a hurry. Sometimes the library’s API is a labyrinthine mess and you just need to shut mypy up about one specific line. Enter the # type: ignore comment.

from some_lib import some_weird_function

result = some_weird_function("hello", 42)  # type: ignore

This tells the type checker, “I see the error on this line, and I am choosing to ignore it. Proceed.” It’s the duct tape of type checking. Useful in a pinch, but a codebase littered with it is a sign you’re losing the war for type safety. Use it sparingly, and preferably with a comment explaining why you’re ignoring it.

# type: ignore[call-arg]  # Ignore because lib's func has wrong arg order
result = some_weird_function("hello", 42)

The Nuclear Option: Monkey Patching Types at Runtime

This is where we get dangerously clever. You can actually reach into the library’s modules at runtime—usually in your application’s startup code—and overwrite the __annotations__ attribute of its classes and functions.

# In your app's __init__.py or main.py
import some_lib

# Patch a function
some_lib.some_function.__annotations__ = {'param': str, 'return': int}

# Patch a class's method
some_lib.SomeClass.method.__annotations__ = {'return': None}

Why you should think twice about this: It’s fragile, it’s shocking to anyone else reading your code, and it can break if the library changes its internal structure. I’ve used this exactly once in production, and I felt like a mad scientist the whole time. It works, but consider it a last resort.

Best Practices and Pitfalls

  1. Start with stubs/: This is the clean, maintainable, and correct approach. It’s separate from your code and from the library itself.
  2. Be Defensive in Stubs: That library function that seems to always return a str? It might return None on a Tuesday. If you’re not 100% sure, use a broader type like Optional[str] or Any. Your stub should reflect reality, not your optimism.
  3. The Any Trap: It’s tempting to just stub everything as returning Any. Don’t. You’ve just moved the problem. Now your type checker trusts that Any value to do anything, completely bypassing type safety. The goal is to contain the untyped code, not to let it leak into your own logic.
  4. Check for Existing Stubs: Before you write a single line, check if someone has already done the work. Projects like typeshed (the repository of stubs for the standard library) and types-some_lib on PyPI are lifesavers. A quick pip install types-some_lib might solve your problem instantly.

The goal here isn’t perfection; it’s damage control. You’re building a bridge between the safe, typed world of your code and the wild west of the untyped ecosystem. A good set of stubs makes that bridge sturdy and reliable. A bunch of # type: ignore comments is just throwing planks over a chasm and hoping for the best.