Skip to content
6 min read

Introduction to MyPy

Introduction to MyPy

I gave an internal talk at DailyBot in early 2021 about MyPy and type checking in Python. At the time, our Python codebase was growing fast — new integrations, new features, new engineers joining the team — and we were starting to feel the pain of working without types.

Function signatures were ambiguous. Return values were a mystery. You’d pass a dict when the function expected a list, and you wouldn’t know until runtime. Tests caught some of this, but not everything. And honestly, writing unit tests just to verify that a function returns a string felt like a waste of time.

MyPy changed that. Not overnight, but gradually. We started adding type hints to critical paths, and the feedback was immediate. Bugs caught at development time. Clearer function contracts. Less time spent digging through code to understand what a function actually does.

This talk was about sharing what we learned and convincing the team that types were worth the effort.


Why We Needed Type Checking at DailyBot

DailyBot integrates with Slack, Microsoft Teams, Google Chat, and a bunch of other platforms. Each integration has its own data structures, webhook payloads, API response formats. When you’re juggling that much external data, it’s very easy for things to break in subtle ways.

Here’s what we were dealing with:

Ambiguous function signatures. You’d look at a function like this:

def process_message(data, user, channel):
    # What are these types? dict? str? object? who knows.
    pass

To understand what data is, you’d have to read the function body, trace back to the caller, maybe check the logs. It was slow and error-prone.

Runtime type errors. You’d pass a None where a str was expected, or a list where a dict was needed. The code would crash in production. Not ideal.

Cognitive load. Every time you touched a function, you had to build a mental model of what types it expected and returned. That’s exhausting when you’re working on a large codebase.

Trivial unit tests. We had tests that literally just checked: “does this function return a dict?” Those tests provided value, but they felt wasteful. The type system should enforce that, not the test suite.


What MyPy Gave Us

MyPy is a static type checker for Python. You add type hints to your code, run MyPy, and it tells you about type mismatches, missing return values, incorrect function calls — all before you run the code.

Here’s what changed after we started using it:

1. Less Cognitive Load

When function signatures are clearly typed, you don’t have to guess. You just read the signature and you know exactly what goes in and what comes out.

Before:

def fetch_user_data(user_id, include_metadata):
    # ???
    pass

After:

def fetch_user_data(user_id: str, include_metadata: bool) -> dict[str, Any]:
    # Crystal clear.
    pass

The second version is self-documenting. No ambiguity.

2. Catch Mistakes Early

Type checking surfaces bugs during development, not in production.

Example: we had a function that was supposed to return a list of user IDs (list[str]), but in one edge case it returned None. Without MyPy, that code would ship and crash when someone iterated over it. With MyPy, it failed the type check immediately:

error: Incompatible return value type (got "None", expected "list[str]")

Fixed before it ever hit staging.

3. Data Validation with attrs

We started using attrs (now evolved into attrs + cattrs) to define typed data classes with runtime validation. This worked beautifully with MyPy.

Example:

from attrs import define

@define
class SlackMessage:
    user_id: str
    channel_id: str
    text: str
    timestamp: float

Now every Slack message payload gets validated at runtime and type-checked at development time. If someone tries to pass an int as user_id, MyPy catches it. If the Slack API sends a malformed payload, attrs catches it.

(We also experimented with Pydantic, which is another great option for data validation + types.)

4. Eliminate Trivial Tests

Before MyPy, we had tests like this:

def test_process_message_returns_dict():
    result = process_message(...)
    assert isinstance(result, dict)

After adding type hints, that test became redundant. MyPy enforces the return type. We deleted dozens of these trivial tests and focused our test suite on behavior, not types.


What Types Can You Use?

Python’s type system is surprisingly rich once you dig into it.

Basic types:

int, str, float, bool, dict, list, set, tuple

Advanced typing (Python 3.5+):

from typing import Any, Callable, Union, Optional, TypeVar, Generic
from typing import Dict, List, Tuple, Set, MutableMapping, NamedTuple

Generics:

def get_first_item(items: list[str]) -> str:
    return items[0]

Optional (nullable):

def find_user(user_id: str) -> Optional[User]:
    # Can return a User or None
    pass

Union (multiple possible types):

def parse_id(value: Union[str, int]) -> int:
    return int(value)

Callable (function types):

def apply_transform(data: dict, transform: Callable[[dict], dict]) -> dict:
    return transform(data)

The more we used these, the clearer our code became.


Integrating MyPy Into Our Workflow

Adding types to an existing codebase is a process. You can’t just flip a switch. Here’s how we rolled it out:

Step 1: Install MyPy

pip install mypy

Step 2: Run MyPy on a Small Module

Start small. Pick one module, add type hints, run MyPy, fix errors. Don’t try to type the entire codebase at once.

mypy src/integrations/slack.py

Step 3: Configure MyPy

We created a mypy.ini config to control strictness. Initially we set it to lenient (allow Any, ignore missing imports) and gradually tightened it.

[mypy]
python_version = 3.9
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = False  # Start lenient, tighten later

Step 4: Add MyPy to CI

Once we had a few modules typed, we added MyPy to our continuous integration pipeline. Every pull request runs MyPy. If types don’t check, the build fails.

# In CI config
- name: Run MyPy
  run: mypy src/

Step 5: Incremental Adoption

We didn’t force everyone to type everything immediately. We set a policy: new code must be typed. Existing code gets typed when you touch it. Over time, coverage grew naturally.


Real-World Example: Slack Integration

Here’s a simplified version of how we typed one of our Slack message handlers.

Before (no types):

def handle_slash_command(payload):
    user_id = payload['user_id']
    command = payload['command']
    text = payload.get('text', '')
    response = process_command(command, text, user_id)
    return response

Lots of questions: What’s in payload? What does process_command return? Can user_id be None?

After (typed):

from typing import Any

def handle_slash_command(payload: dict[str, Any]) -> dict[str, str]:
    user_id: str = payload['user_id']
    command: str = payload['command']
    text: str = payload.get('text', '')
    response: dict[str, str] = process_command(command, text, user_id)
    return response

Now it’s explicit. payload is a dict, response is a dict with string keys and values, and user_id is a string. MyPy verifies all of it.

Even better with attrs:

from attrs import define

@define
class SlashCommandPayload:
    user_id: str
    command: str
    text: str = ''

def handle_slash_command(payload: SlashCommandPayload) -> dict[str, str]:
    response = process_command(payload.command, payload.text, payload.user_id)
    return response

Now the payload structure is a first-class type. Impossible to mess up.


What I Learned

Type checking isn’t about being pedantic. It’s about reducing the mental overhead of working in a large codebase. When I can look at a function signature and immediately understand what it does without reading the implementation, that’s a huge win.

It’s also about catching mistakes before they matter. Finding a type error in CI is way better than finding it in a production error log.

At DailyBot, MyPy became a standard part of our workflow. New engineers loved it because it made onboarding easier — they could explore the codebase without constantly asking “what type is this?” Experienced engineers loved it because it reduced the number of silly bugs that slipped through code review.

If you’re working on a Python project of any significant size, I’d recommend giving MyPy a shot. Start small, add types incrementally, and see how it changes your development experience.

Let’s keep building.


Resources