Type-safe mocking for pytest - preserve original type hints while using mock features
Project description
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_countare 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 environmentcircular import- Your codebase has circular imports (fix withTYPE_CHECKINGblocks)
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)
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
targets = [
"myapp.services.*", # All classes in myapp.services
"myapp.repositories.ProductRepository", # 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",
]
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
typed-pytest-generator -t "myapp.services.*"
# Combine options
typed-pytest-generator -t "myapp.services.*" -e myapp.services.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"]
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file typed_pytest-0.1.1.tar.gz.
File metadata
- Download URL: typed_pytest-0.1.1.tar.gz
- Upload date:
- Size: 66.1 kB
- Tags: Source
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
221db86a334ee8f9d0649b3a83a08e349067f0e86f59d90a7da2aaa7df737cfe
|
|
| MD5 |
c2cc1ddc6f50bab90b4db86bf65c751e
|
|
| BLAKE2b-256 |
858c8e14a8aeb23eb9ef24a39b9137c9e495e8cef6ac7e94e7b84dbca7271e79
|
File details
Details for the file typed_pytest-0.1.1-py3-none-any.whl.
File metadata
- Download URL: typed_pytest-0.1.1-py3-none-any.whl
- Upload date:
- Size: 32.6 kB
- Tags: Python 3
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3cea330627610bba149dbf18c0556127421d8939b677858c8cfe23294c4d53ed
|
|
| MD5 |
affe537297ea87d36df3b3c59cb22403
|
|
| BLAKE2b-256 |
367de4cb376e3644c8b44d3283bd51650d15cf70bac4bf5cdf0cddd7f73eae22
|