Right, let’s talk about getting serious with your type checker. You’ve dipped your toes in, added a few str and int annotations, and maybe mypy has stopped yelling at you. Congrats, you’re typing. But you’re not type checking. There’s a vast, desolate chasm between the two, and on the other side lies maintainable, robust, and frankly, less-infuriating code. The bridge across that chasm is called “strict Mode.”

Think of your type checker’s default settings as training wheels. They’re there so you don’t get overwhelmed and give up entirely. Strict mode is when you, an adult, take the training wheels off, throw them in the dumpster, and set the dumpster on fire. It’s a collection of settings that force the type checker to be pedantic, exhaustive, and actually useful. Without it, you’re just giving your code a suggestive glance, not a real audit.

Why You Absolutely Want Strict Mode

You don’t just want it; you need it. Enabling strict mode is the single most impactful thing you can do to get real value from type hints. The default rules are, to put it bluntly, full of holes you could drive a truck through. They allow a shocking amount of dynamic, untyped nonsense to slip through, which completely defeats the purpose. Strict mode plugs those holes. It transforms your type checker from a friendly linter that occasionally makes suggestions into a ruthless logic engine that finds bugs before they happen. I’ve lost count of the subtle, nasty issues strict mode has caught for me—issues that would have caused a runtime exception hours later, buried under layers of application logic. It’s like having a brilliant, hyper-vigilant code reviewer who never sleeps and never gets tired of your crap.

Enabling it is usually a flags game. For mypy, you can either use the --strict flag (which enables a curated set of the most important rules) or, my preferred method, be explicit in your config file.

# mypy.ini or pyproject.toml
[mypy]
strict = True

# OR, for fine-grained control (which I recommend for large existing codebases)
disallow_untyped_defs = True
disallow_incomplete_defs = True
disallow_any_generics = True
no_implicit_optional = True
check_untyped_defs = True
warn_return_any = True
warn_unused_ignores = True
# ...and so on

For pyright, you’ll set it in your pyproject.toml or pyrightconfig.json.

// pyrightconfig.json
{
  "reportMissingImports": true,
  "reportUnknownVariableType": true,
  "reportUnknownMemberType": true,
  "strictListInference": true,
  "strictDictionaryInference": true,
  "strictSetInference": true
  // Pyright's "strict" is more about enabling individual strict settings.
}

The Art of the Incremental Check

Now, you’ve just enabled strict mode on your 200,000-line codebase. It immediately eruptes into 50,000 errors. Your first instinct is to panic. Your second is to turn it back off. Don’t do either. This is where incremental adoption becomes your best friend.

You don’t have to fix everything at once. That’s a recipe for burnout and a mutiny from your team. The strategy is to contain the explosion. Use # type: ignore comments with specific error codes on particularly nasty lines you can’t fix right now. This is a tactical retreat, not a surrender. More importantly, use your config file to disable strict mode for specific directories or modules that you’re not ready to tackle. This allows you to enforce strict rules on all new code while gradually cleaning up the old stuff.

# In mypy.ini, make the legacy/ directory a safe haven for mypy's wrath
[mypy-legacy.*]
disallow_untyped_defs = False
ignore_errors = True

This approach is how large companies like Dropbox migrated. They didn’t do a giant, all-or-nothing refactor; they slowly expanded the “strict” territory until it conquered the entire codebase.

mypy --strict vs. Pyright’s Strictness

It’s important to know that “strict” isn’t a single standard. mypy --strict is a predefined bundle of around a dozen flags that the mypy team considers essential. Pyright, on the other hand, tends to be more strict out of the box in many areas, but its “strict” settings are often more granular, individual options you can enable. For example, Pyright is famously more aggressive and intelligent about type narrowing than mypy. This isn’t a case of one being better; it’s a case of them having different philosophies. mypy’s approach lets you get to a known “good enough” state quickly with one flag, while Pyright gives you more fine-tuned control from the start. Try both. You might find your team prefers the character of one over the other.

Common Strict Mode Pitfalls (And How to Jump Them)

You’ll hit a few things constantly. Here’s how to handle them:

  1. implicit_optional: This one gets everyone. By default, mypy treats def foo(bar: str = None) as bar: Optional[str] = None. With no_implicit_optional = True, it rightfully complains. This is a good thing! It forces you to be explicit. The fix is simple: just write Optional[str]. It’s clearer and prevents bugs.

  2. disallow_any_generics: This forbids using unparameterized generic types like list or dict. You must write list[str] and dict[str, int]. This is non-negotiable for good type safety. Using the raw generic type is basically like using Any; it tells the type checker nothing about what’s inside the container.

  3. The # type: ignore Comment: This is your escape hatch, but use it wisely. Always, always specify the error code you’re ignoring. A bare # type: ignore silences everything on that line, which is a great way to hide a future, critical error.

    # Bad: Silences all possible errors on this line. Lazy and dangerous.
    something_that_errors()  # type: ignore
    
    # Good: Only silences the specific "call-to-untyped-function" error.
    something_that_errors()  # type: ignore[no-untyped-call]
    

The goal isn’t to have zero errors; it’s to have known, justified, and contained errors. You’re moving from a codebase that is unknowingly broken to one that is knowingly and measurably imperfect, which is a massive leap forward in maturity. Now go turn it on. I’ll wait.