Why Dropbox Chose Static Typing
Python is the most widely used language at Dropbox, powering both backend services and the desktop client. But at the scale of millions of lines of code, dynamic typing became a bottleneck. The core issue is comprehension: without type annotations, answering basic questions about a function—can it return None? What should items be? Is id an int, a str, or something else?—often requires reading implementation details or trusting unreliable docstrings.
class Resource:
id: bytes
...
def read_metadata(self,
items: Sequence[str]) -> Dict[str, MetadataItem]:
...
With annotations, these answers are explicit and, crucially, verifiable. A type checker like mypy provides what amounts to validated documentation. It also catches bugs—such as forgetting to handle a None value—and makes refactoring far safer, since the checker pinpoints exactly which code needs changes. In a large codebase, running a full type check can take under a second, offering much quicker feedback than a test suite that might take minutes. IDEs like PyCharm and Visual Studio Code also leverage type annotations for better completion, error highlighting, and navigation.
From Research Project to Python Standard
Mypy's origins trace back to Jukka Lehtosalo's PhD research in Cambridge, which explored unifying statically typed and dynamically typed languages through gradual typing. The initial work centered on a custom language called Alore, used to test type-checking concepts. Because Alore was inspired by Python, it was straightforward to adapt the checker to target Python syntax and run experiments on real open-source code. A source-to-source translator from Alore to Python produced a type checker written in Python itself.
def Fib(n as Int) as Int
if n <= 1
return n
else
return Fib(n - 1) + Fib(n - 2)
end
end
The initial versions used a hybrid Python-Java syntax due to limitations in early Python 3 annotation support. A key turning point came at PyCon 2013, when Guido van Rossum convinced Lehtosalo to abandon the custom syntax in favor of standard Python:
def fib(n: int) -> int:
if n <= 1:
return n
else:
return fib(n - 1) + fib(n - 2)
Some sacrifices were necessary—variable annotations were unavailable until Python 3.6, so type comments were used as a stopgap:
products = [] # type: List[str] # Eww
These comments also enabled Python 2 support. The decision to stay within standard Python syntax proved wise: existing tools worked unchanged, and adoption was smoother.
Standardizing Type Hints
Early experiments at Dropbox during Hack Week 2014 showed promise, and soon after, work began on standardizing type hint semantics across the Python ecosystem. The result was PEP 484, co-authored by Guido van Rossum, Łukasz Langa, and Lehtosalo, which shipped with Python 3.5 in 2015. The goal was to prevent fragmented, incompatible typing approaches and to engage the wider community in the design. While there was initial suspicion from a community known for duck typing, that subsided as it became clear that type hinting would remain optional and useful.
Scaling Up with Incremental Checking
A dedicated three-person team—including Guido—began working on mypy at Dropbox in late 2015. The immediate challenge was performance. With the upstream compiler idea scrapped, mypy ran on the CPython interpreter, which is not fast enough for a tool like this in large repos.
The first major breakthrough was incremental checking. If a module's dependencies haven't changed since the last run, mypy reuses cached data from its previous analysis. If a module's external interface remains unchanged, then importing modules don't need to be reprocessed at all. To help further, remote caching was added: when the local cache is stale, mypy downloads a fresh cache snapshot for the whole codebase, then builds on top of it. This proved especially valuable during bulk annotation efforts, which require many iterative runs to refine types.
Adoption grew quickly through 2016, reaching about 420,000 annotated lines by year's end. User surveys showed two clear priorities: wider coverage and faster runtimes. Those directed the next phase of work.
From minutes to seconds: the mypy daemon
Incremental builds made mypy faster, but many runs still took about a minute. The culprit was a familiar one in large Python codebases: cyclic imports. Dropbox had sets of hundreds of modules that imported each other indirectly. A change to any file in such a cycle forced mypy to reprocess every module in it, plus anything that imported from it. The largest of these cycles—the “tangle”—contained several hundred modules and was imported by a large portion of the codebase.
Breaking the tangle wasn't feasible; there was too much unfamiliar code to refactor. Instead, Dropbox built the mypy daemon to make mypy fast despite the tangles. The daemon is a server process that keeps the entire codebase's type information in memory, so runs don't reload cache data for thousands of dependencies. More importantly, it tracks fine-grained dependencies between constructs. If function foo calls bar, a dependency is recorded from bar to foo. When a file changes, the daemon first processes only that file, then looks for externally visible changes—such as a modified function signature—and rechecks only the functions that actually consume the changed ones, which is usually a small set.
The implementation required reworking an architecture originally built to process one file at a time. Numerous edge cases arose around what needs reprocessing when, for example, a class gains a new base class. After careful work, most incremental runs dropped to a few seconds.
Fast even from cold: mypyc
The daemon, together with remote caching, solved the iterative case. Worst-case performance, however, remained poor: a clean mypy build took over 15 minutes and grew worse each week as new code and annotations appeared.
Dropbox revisited an early mypy idea: compiling Python to C. Experiments with Cython yielded no speedup, so the team decided to write a compiler. The mypy codebase itself was fully annotated, so the types could drive code generation. A proof-of-concept prototype showed performance improvements over 10x on micro-benchmarks. The design compiled Python modules to CPython C extensions and turned type annotations into runtime type checks—the checked language happened to look and behave exactly like Python, which enabled a codebase migration without changing the language.
Targeting the CPython extension API kept scope manageable. No VM or libraries were needed, and the entire Python ecosystem, including tools like pytest, remained available. During development, interpreted Python could still be used for rapid edit-test cycles.
The compiler, named mypyc (it uses mypy as a front end for type analysis), delivered around a 4x speedup on clean mypy runs. The core effort took about four calendar months with a small team. The compiler translates compiled function calls into C calls, eliminating interpretation overhead. Some operations, like dictionary lookups, still fall back to generalized CPython C API calls and see only marginal gains. Profiling identified the most common of these slow operations; the team then either tuned mypyc to generate faster C code or rewrote the Python to use faster constructs—often easier than automating the transformation in the compiler.
Coverage up to 4 million lines
Increasing annotation coverage was the second most requested feature in mypy user surveys. Dropbox combined several approaches—organic growth, manual effort, and static inference—to reach almost 4 million annotated lines in its largest back-end repository in about three years.
Mypy gained coverage reports that track progress and highlight sources of type imprecision, including explicit unchecked Any annotations and imports of third-party libraries lacking annotations. Dropbox also contributed improved stub files for popular open-source libraries to typeshed and standardized new type system features through PEPs. Notable among these is TypedDict, which types JSON-like dictionaries with fixed string keys of distinct value types.
Key efforts to increase coverage included:
- Strictness. Requirements for annotations tightened gradually, starting with linter advice for files that already had some, then becoming a requirement for new and most existing Python files.
- Coverage reporting. Weekly emails highlighted teams’ coverage levels and suggested high-value annotation targets.
- Outreach. Talks and direct chats helped teams adopt mypy.
- Surveys. Recurring user surveys identified pain points, prioritized for resolution.
- Performance. The daemon and mypyc reduced p75 latency 44x, lowering friction for annotation workflows.
- Editor integrations. Support for PyCharm, Vim, and VS Code made iterating on annotations easier.
- Static analysis. An internal tool inferred signatures for simple cases, boosting coverage at low cost.
- Third-party library support. A PEP 561 stub package and mypy plugin were built for SQLAlchemy, whose dynamic patterns don't fit PEP 484 directly.
Bumps along the way
Reaching the 4-million-line milestone surfaced several mistakes worth learning from.
Missing files. Only a small number of files were in the initial mypy build; everything else was unchecked, so imports from unbuilt modules produced unverified Any values. Adding a file to the build often exposed incompatible annotations elsewhere, forcing widespread corrections. Bringing basic library modules into the build earlier would have smoothed the path.
Annotating legacy code. The codebase had over 4 million lines of pre-existing Python. Dropbox implemented PyAnnotate, which collects runtime types during tests and inserts annotations, but it saw little adoption. Type collection was slow, and generated types needed considerable manual cleanup. Most annotations were ultimately written by code owners, guided by reports highlighting high-use modules worth annotating first.
Import cycles. Besides slowing mypy, import cycles made supporting idioms difficult. A major redesign finally fixed most cycle-related issues. The root cause traced back to the early days of Alore, the research language that pre-dated mypy’s Python support. Alore’s syntax made cycles easy to handle because statements weren't ambiguous in context. Python, however, lets an assignment define a type alias, and mypy can’t always detect that until most of an import cycle is processed. Early design decisions, it turned out, can exact interest years later.
Beyond four million
What started as experimental prototypes has become a production type-checking setup covering four million lines of Python. The ripple effects extend well beyond Dropbox: type hinting is now a standardized part of Python, IDE support is commonplace, and the ecosystem offers multiple type checkers with different tradeoffs alongside libraries that embrace typing.
At Dropbox, type checking is simply part of how Python is written now. But the broader community is still in the early phase of this transition. The trajectory points toward continued growth in tooling and adoption. For teams running large Python codebases that haven't yet introduced type checking, the present is a reasonable entry point. Colleagues who have made the move generally haven't looked back; for large projects, it makes Python substantially more practical.



