Skip to main content

typed-pytest

MagicMock kills your IDE autocomplete. We fix that.

Type-safe mocking for pytest with full IDE support - autocomplete for method names, parameters, and mock assertions.

https://github.com/user-attachments/assets/f542f815-731b-4398-a8b6-fefadef19d7b

Problem

When using pytest with unittest.mock, type inference becomes problematic:

from unittest.mock import MagicMock
from typing import cast

class UserService:
    def get_user(self, user_id: int) -> dict:
        return {"id": user_id, "name": "John"}

# Problem 1: MagicMock loses original type hints
mock_service = MagicMock(spec=UserService)
mock_service.get_user(1)  # No autocomplete for get_user parameters

# Problem 2: cast() loses mock method hints
service = cast(UserService, mock_service)
service.get_user.assert_called_once()  # assert_called_once has no type hint!

Solution

typed-pytest provides type-safe mocking with:

  • Catch typos at lint time - Misspelled method names are caught by mypy/pyright before running tests
  • Original class method signatures - Full auto-completion for method names and parameters
  • Type-checked mock assertions - assert_called_once_with() and other assertions have full type hints
  • Type-checked mock properties - return_value, side_effect, call_count are properly typed
  • IDE auto-completion for both original methods and mock methods
from typed_pytest_stubs import typed_mock, UserService

mock = typed_mock(UserService)
mock.get_usr  # ❌ Caught by type checker: "get_usr" is not a known member
mock.get_user.assert_called_once_with(1)  # ✅ Type-checked!
from typed_pytest import TypedMock, typed_mock

mock_service: TypedMock[UserService] = typed_mock(UserService)

# Mock methods have type hints
mock_service.get_user.assert_called_once_with(1)  # Type-checked!
mock_service.get_user.return_value = {"id": 1}    # Type-checked!

Installation

# Using uv
uv add typed-pytest

# Using pip
pip install typed-pytest

Requirements

  • Python 3.10+
  • pytest 8.0+
  • pytest-mock 3.11+

Quick Start

from typed_pytest import typed_mock, TypedMock, TypedMocker

# Method 1: Direct mock creation
def test_user_service():
    mock_service: TypedMock[UserService] = typed_mock(UserService)
    mock_service.get_user.return_value = {"id": 1, "name": "Test"}

    result = mock_service.get_user(1)

    assert result == {"id": 1, "name": "Test"}
    mock_service.get_user.assert_called_once_with(1)

# Method 2: Using pytest fixture
def test_with_fixture(typed_mocker: TypedMocker):
    mock_service = typed_mocker.mock(UserService)
    mock_service.get_user.return_value = {"id": 1}

    mock_service.get_user(1)
    mock_service.get_user.assert_called_once_with(1)

Stub Generator

typed-pytest-generator generates stub files for IDE auto-completion support. This allows your IDE to provide method signatures and type hints when using typed_mock().

Important: Environment Requirements

The generator must run in your project's virtual environment because it imports your classes to inspect their method signatures. This means:

# ✅ Correct: Run in project environment (has access to your dependencies)
uv add typed-pytest --dev
uv run typed-pytest-generator

# ❌ Wrong: uvx runs in isolated environment (no access to your dependencies)
uvx typed-pytest-generator  # Will fail if your classes import sqlalchemy, etc.

Why? The generator uses Python's inspect module to extract method signatures, which requires actually importing your classes. If your classes depend on sqlalchemy, pydantic, or other packages, those must be installed in the environment where the generator runs.

Common errors:

  • No module named 'sqlalchemy' - Your class imports a package not in the isolated environment
  • circular import - Your codebase has circular imports (fix with TYPE_CHECKING blocks)

Basic Usage

# Generate stubs for your classes
typed-pytest-generator -t myapp.services.UserService myapp.repos.ProductRepository

# Specify custom output directory
typed-pytest-generator -t myapp.services.UserService -o my_stubs

# Include private methods (starting with _)
typed-pytest-generator -t myapp.services.UserService --include-private

# Verbose output
typed-pytest-generator -t myapp.services.UserService -v

Generated Files

The generator creates the following structure:

typed_pytest_stubs/
├── __init__.py      # Re-exports all stub classes
└── _runtime.py      # Runtime class definitions with method signatures

Using Generated Stubs

# Import typed_mock from stubs package for full auto-completion
from typed_pytest_stubs import typed_mock, UserService

def test_user_service():
    # typed_mock returns UserService_TypedMock with full IDE support
    mock = typed_mock(UserService)

    mock.get_user              # ✅ Auto-complete for method names
    mock.get_user.return_value # ✅ Auto-complete for mock properties
    mock.get_user.assert_called_once_with(1)  # ✅ Type-checked!

    mock.get_user.return_value = {"id": 1, "name": "Test"}
    result = mock.get_user(1)

Backend Selection

The generator supports two backends for extracting type information:

Backend Speed Return Types Use Case
inspect (default) Fast (~10ms/class) typing.Any Quick iteration during development
stubgen Slower (~500ms/class) Preserved (dict[str, Any], bool, etc.) Production, CI, better type hints
# Use default inspect backend (fast)
typed-pytest-generator -t myapp.services.UserService

# Use stubgen backend for better type information
typed-pytest-generator --backend stubgen -t myapp.services.UserService

# Short form
typed-pytest-generator -b stubgen -t myapp.services.UserService

Example output difference:

With inspect backend:

class UserService_TypedMock:
    @property
    def get_user(self) -> MockedMethod[[int], typing.Any]: ...

With stubgen backend:

class UserService_TypedMock:
    @property
    def get_user(self) -> MockedMethod[[int], dict[str, Any]]: ...

Configuration

pyproject.toml Configuration

You can configure typed-pytest-generator in your pyproject.toml file. This allows you to define targets, output directory, and other options without passing them via CLI every time.

[tool.typed-pytest-generator]
# List of fully qualified class names to generate stubs for
# Supports wildcard patterns:
#   - "module.*"  matches all classes in the module (non-recursive)
#   - "module.**" matches all classes in the package recursively (includes submodules)
targets = [
    "myapp.services.*",                      # All classes in myapp.services module
    "myapp.repositories.**",                 # All classes in repositories and all submodules
    "myapp.models.User",                     # Specific class
]

# Output directory for generated stubs (default: "typed_pytest_stubs")
output-dir = "typed_pytest_stubs"

# Include private methods starting with _ (default: false)
include-private = false

# Exclude specific classes from stub generation
exclude-targets = [
    "myapp.internal.PrivateHelper",
    "myapp.legacy.DeprecatedService",
]

# Backend for type extraction (default: "inspect")
# - "inspect": Fast, uses Python's inspect module
# - "stubgen": Slower, uses mypy's stubgen for accurate return types
backend = "inspect"

Once configured, simply run:

# Uses configuration from pyproject.toml (auto-discovered)
typed-pytest-generator

# With verbose output
typed-pytest-generator -v

CLI Options

CLI arguments take precedence over pyproject.toml configuration:

# Override targets from config
typed-pytest-generator -t myapp.services.NewService

# Override output directory
typed-pytest-generator -o custom_stubs

# Add exclusions (merged with config exclusions)
typed-pytest-generator -e myapp.services.SkipThis

# Use a specific config file
typed-pytest-generator -c /path/to/pyproject.toml

# Use wildcard to match all classes in a module (non-recursive)
typed-pytest-generator -t "myapp.services.*"

# Use recursive wildcard to match all classes in a package and submodules
typed-pytest-generator -t "myapp.repositories.**"

# Combine options
typed-pytest-generator -t "myapp.repositories.**" -e myapp.repositories.Internal -o stubs -v

IDE and Type Checker Setup

Add typed_pytest_stubs/ to your .gitignore since these files are generated:

# Generated stub files
typed_pytest_stubs/

For pyright/pylance users, add the stubs directory to your pyproject.toml:

[tool.pyright]
include = ["src", "tests", "typed_pytest_stubs"]

Type Checker Configuration for Generated Stubs

Since typed_pytest_stubs/ is typically gitignored, some type checkers need extra configuration to find the generated stubs.

pyrefly (Meta's type checker)

Add the project root to search-path:

[tool.pyrefly]
project-includes = ["src", "tests"]
# Add project root so typed_pytest_stubs can be found
search-path = ["."]

ty (Astral's type checker)

Add the project root to extra-paths:

[tool.ty.environment]
# Add project root so typed_pytest_stubs can be found
extra-paths = ["."]

mypy and pyright

These work out of the box since they auto-discover packages in the project root.

Recommended Workflow

The generated stubs should not be committed to Git. Instead, generate them:

  1. Before running tests locally

    typed-pytest-generator && pytest
    
  2. In your CI pipeline

    # GitHub Actions example
    - name: Generate test stubs
      run: uv run typed-pytest-generator
    
    - name: Run tests
      run: uv run pytest
    
  3. As a Makefile target (recommended)

    test:
        uv run typed-pytest-generator
        uv run pytest
    

Why not pre-commit hook or Git?

  • Generated code causes unnecessary Git conflicts
  • Stubs should always reflect the current source code
  • Keeps PRs clean and focused on actual changes

Development

# Clone repository
git clone https://github.com/tmdgusya/typed-pytest.git
cd typed-pytest

# Install dependencies
uv sync --all-extras

# Run tests
uv run pytest

# Run tests with coverage
uv run pytest --cov=src/typed_pytest --cov-report=term-missing

# Type check
uv run mypy src/
uv run pyright src/

# Lint
uv run ruff check src/ tests/
uv run ruff format src/ tests/

Documentation

License

MIT

Metadata

Release files for typed-pytest 0.1.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for typed-pytest 0.1.8
File Size Uploaded Provenance
typed_pytest-0.1.8.tar.gz 77.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for typed-pytest 0.1.8
File Interpreter ABI Platform
typed_pytest-0.1.8-py3-none-any.whl Python 3 none any Details

Total release size: 117.0 kB

Release files / typed_pytest-0.1.8.tar.gz

Download URL typed_pytest-0.1.8.tar.gz
Size 77.1 kB
Tags Source
SHA-256 checksum
How to use checksums
b72c1d612652817d0d2f395ffac91ccdc3f1f6444f4aa05d35ed843750fcad60
BLAKE2b-256 checksum
How to use checksums
caa4b089efab7bd357cb0dc67be1d154f463c18390e5ad89ca5fcaafdf32bdb9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / typed_pytest-0.1.8-py3-none-any.whl

Download URL typed_pytest-0.1.8-py3-none-any.whl
Size 39.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a3e7ab57f016fc3e1a338b94c6fb10ae463b12b09a97101dd857e9b7c5c6bc0d
BLAKE2b-256 checksum
How to use checksums
ffd8140a07c58166417f153af3ad8c4868d9d61c05bcc29d1fd372b8f71b66d5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for typed-pytest 0.1.8

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page