mypy-protobuf 5.0.0
Generate mypy stub files from protobuf specs
License: Apache License 2.0
Requires Python: >=3.8
Keywords: mypy proto dropbox

mypy-protobuf: Generate mypy stub files from protobuf specs

[![CI](https://github.com/nipunn1313/mypy-protobuf/workflows/CI/badge.svg)](https://github.com/nipunn1313/mypy-protobuf/actions?query=branch%3Amain)
[![pypi](https://img.shields.io/pypi/v/mypy-protobuf?logo=Pypi)](https://pypi.org/project/mypy-protobuf/)
[![license](https://img.shields.io/github/license/nipunn1313/mypy-protobuf)](https://github.com/nipunn1313/mypy-protobuf/blob/main/LICENSE)
===========================================================

2.10 is the last version of mypy-protobuf which supports targeting python 2.7.

Built originally with love at [Dropbox](https://github.com/dropbox)

See [Changelog](CHANGELOG.md) for recent changes.

## Requirements to run mypy-protobuf

Earlier releases might work, but aren't tested

- [protoc >= 32.0](https://github.com/protocolbuffers/protobuf/releases)
- [python-protobuf >= 6.32](https://pypi.org/project/protobuf/) - matching protoc release
- [python >= 3.9](https://www.python.org/downloads/source/) - for running mypy-protobuf plugin.

## Requirements to run typecheckers on stubs generated by mypy-protobuf

Earlier releases might work, but aren't tested

- [mypy >= v1.14.0](https://pypi.org/project/mypy) or [pyright >= 1.1.383](https://github.com/microsoft/pyright)
- [python-protobuf >= 6.32](https://pypi.org/project/protobuf/) - matching protoc release
- [types-protobuf >= 6.32](https://pypi.org/project/types-protobuf/) - for stubs from the google.protobuf library

### To run typecheckers on code generated with grpc plugin - you'll additionally need

Earlier releases might work, but aren't tested

- [grpcio>=1.70](https://pypi.org/project/grpcio/)
- [grpcio-tools>=1.70](https://pypi.org/project/grpcio-tools/)
- [types-grpcio>=1.0.0.20251009](https://pypi.org/project/types-grpcio/)

Other configurations may work, but are not continuously tested currently.
We would be open to expanding this list - file an issue on the issue tracker.

## Installation

The plugin can be installed with

```
pip3 install mypy-protobuf
```

To install unreleased

```
REV=main  # or whichever unreleased git rev you'd like
pip3 install git+https://github.com/nipunn1313/mypy-protobuf.git@$REV

# For older (1.x) versions of mypy protobuf - you may need
pip3 install git+https://github.com/nipunn1313/mypy-protobuf.git@$REV#subdirectory=python
```

In order to run mypy on the generated code, you'll need to install

```
pip3 install mypy>=0.910 types-protobuf>=0.1.14
```

# Usage

On posix, protoc-gen-mypy is installed to python's executable bin. Assuming that's
on your $PATH, you can run

```
protoc --python_out=output/location --mypy_out=output/location
```

Alternately, you can explicitly provide the path:

```
protoc --plugin=protoc-gen-mypy=path/to/protoc-gen-mypy --python_out=output/location --mypy_out=output/location
```

Check the version number with

```
> protoc-gen-mypy --version
```

## Implementation

The implementation of the plugin is in `mypy_protobuf/main.py`, which installs to
an executable protoc-gen-mypy. On windows it installs to `protoc-gen-mypy.exe`

## Features

See [Changelog](CHANGELOG.md) for full listing

### Differences between the protobuf compiler (`--pyi_out`) and `mypy-protobuf`

(As of 11/17/2025)

* `mypy-protobuf` generates stubs for GRPC services and clients
* `mypy-protobuf` generates correctly typed `HasField` methods depending on field presence, `pyi_out` does not type `HasField` arguments
* `mypy-protobuf` generates correctly typed constructors dependinding on field presence.
* `mypy-protobuf` generates correctly typed `HasField`, `WhichOneof`, and `ClearField` methods.
* There are differences in how `mypy-protobuf` and `pyi_out` generate enums. See [this issue](https://github.com/protocolbuffers/protobuf/issues/8175) for details
* Type aliases exported for `HasField`, `WhichOneof` and `ClearField` arguments
* Parses comments as docstrings
* `mypy-protobuf` marks enums, enum values, messages, message fields, services, and methods with `@warnings.deprecated` if the deprecation option is set to true.

#### Examples

`mypy-protobuf`:

```python
"""
@generated by mypy-protobuf.  Do not edit manually!
isort:skip_file
Edition version of proto2 file"""

import builtins
import google.protobuf.descriptor
import google.protobuf.message
import sys
import typing

if sys.version_info >= (3, 10):
    import typing as typing_extensions
else:
    import typing_extensions

DESCRIPTOR: google.protobuf.descriptor.FileDescriptor

@typing.final
class Editions2024SubMessage(google.protobuf.message.Message):
    DESCRIPTOR: google.protobuf.descriptor.Descriptor

    THING_FIELD_NUMBER: builtins.int
    thing: builtins.str
    def __init__(
        self,
        *,
        thing: builtins.str | None = ...,
    ) -> None: ...
    _HasFieldArgType: typing_extensions.TypeAlias = typing.Literal["thing", b"thing"]
    def HasField(self, field_name: _HasFieldArgType) -> builtins.bool: ...
    _ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["thing", b"thing"]
    def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___Editions2024SubMessage: typing_extensions.TypeAlias = Editions2024SubMessage

@typing.final
class Editions2024Test(google.protobuf.message.Message):
    DESCRIPTOR: google.protobuf.descriptor.Descriptor

    LEGACY_FIELD_NUMBER: builtins.int
    EXPLICIT_SINGULAR_FIELD_NUMBER: builtins.int
    MESSAGE_FIELD_FIELD_NUMBER: builtins.int
    IMPLICIT_SINGULAR_FIELD_NUMBER: builtins.int
    DEFAULT_SINGULAR_FIELD_NUMBER: builtins.int
    legacy: builtins.str
    """Expect to be always set"""
    explicit_singular: builtins.str
    """Expect HasField generated"""
    implicit_singular: builtins.str
    """Expect implicit field presence, no HasField generated"""
    default_singular: builtins.str
    """Not set, should default to EXPLICIT"""
    @property
    def message_field(self) -> Global___Editions2024SubMessage:
        """Expect HasField generated?"""

    def __init__(
        self,
        *,
        legacy: builtins.str | None = ...,
        explicit_singular: builtins.str | None = ...,
        message_field: Global___Editions2024SubMessage | None = ...,
        implicit_singular: builtins.str = ...,
        default_singular: builtins.str | None = ...,
    ) -> None: ...
    _HasFieldArgType: typing_extensions.TypeAlias = typing.Literal["default_singular", b"default_singular", "explicit_singular", b"explicit_singular", "legacy", b"legacy", "message_field", b"message_field"]
    def HasField(self, field_name: _HasFieldArgType) -> builtins.bool: ...
    _ClearFieldArgType: typing_extensions.TypeAlias = typing.Literal["default_singular", b"default_singular", "explicit_singular", b"explicit_singular", "implicit_singular", b"implicit_singular", "legacy", b"legacy", "message_field", b"message_field"]
    def ClearField(self, field_name: _ClearFieldArgType) -> None: ...

Global___Editions2024Test: typing_extensions.TypeAlias = Editions2024Test

```

Builtin pyi generator:

```python
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from collections.abc import Mapping as _Mapping
from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union

DESCRIPTOR: _descriptor.FileDescriptor

class Editions2024SubMessage(_message.Message):
    __slots__ = ()
    THING_FIELD_NUMBER: _ClassVar[int]
    thing: str
    def __init__(self, thing: _Optional[str] = ...) -> None: ...

class Editions2024Test(_message.Message):
    __slots__ = ()
    LEGACY_FIELD_NUMBER: _ClassVar[int]
    EXPLICIT_SINGULAR_FIELD_NUMBER: _ClassVar[int]
    MESSAGE_FIELD_FIELD_NUMBER: _ClassVar[int]
    IMPLICIT_SINGULAR_FIELD_NUMBER: _ClassVar[int]
    DEFAULT_SINGULAR_FIELD_NUMBER: _ClassVar[int]
    legacy: str
    explicit_singular: str
    message_field: Editions2024SubMessage
    implicit_singular: str
    default_singular: str
    def __init__(self, legacy: _Optional[str] = ..., explicit_singular: _Optional[str] = ..., message_field: _Optional[_Union[Editions2024SubMessage, _Mapping]] = ..., implicit_singular: _Optional[str] = ..., default_singular: _Optional[str] = ...) -> None: ...
```

### Bring comments from .proto files to docstrings in .pyi files

Comments in the .proto files on messages, fields, enums, enum variants, extensions, services, and methods
will appear as docstrings in .pyi files. Useful in IDEs for showing completions with comments.

### Types enum int values more strongly

Enum int values produce stubs which wrap the int values in NewType

```proto
enum MyEnum {
  HELLO = 0;
  WORLD = 1;
}
```

Will yield an [enum type wrapper](https://github.com/python/typeshed/blob/16ae4c61201cd8b96b8b22cdfb2ab9e89ba5bcf2/stubs/protobuf/google/protobuf/internal/enum_type_wrapper.pyi) whose methods type to `MyEnum.ValueType` (a `NewType(int)` rather than `int`.
This allows mypy to catch bugs where the wrong enum value is being used.

Calling code may be typed as follows.

In python >= 3.7

```python
# May need [PEP 563](https://www.python.org/dev/peps/pep-0563/) to postpone evaluation of annotations
# from __future__ import annotations  # Not needed with python>=3.11 or protobuf>=3.20.0
def f(x: MyEnum.ValueType):
    print(x)
f(MyEnum.Value("HELLO"))
```

With protobuf <= 3.20.0, for usages of cast, the type of `x` must be quoted
After protobuf >= 3.20.0 - `ValueType` exists in the python code and quotes aren't needed
until [upstream protobuf](https://github.com/protocolbuffers/protobuf/pull/8182) includes `ValueType`

```python
cast('MyEnum.ValueType', x)
```

Similarly, for type aliases with protobuf < 3.20.0, you must either quote the type or hide it behind `TYPE_CHECKING`

```python
from typing import Tuple, TYPE_CHECKING
HELLO = Tuple['MyEnum.ValueType', 'MyEnum.ValueType']
if TYPE_CHECKING:
    HELLO = Tuple[MyEnum.ValueType, MyEnum.ValueType]
```

#### Enum int impl details

mypy-protobuf autogenerates an instance of the EnumTypeWrapper as follows.

```python
class _MyEnum:
    ValueType = typing.NewType('ValueType', builtins.int)
    V: typing_extensions.TypeAlias = ValueType
class _MyEnumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_MyEnum.ValueType], builtins.type):
    DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
    HELLO: _MyEnum.ValueType  # 0
    WORLD: _MyEnum.ValueType  # 1
class MyEnum(_MyEnum, metaclass=_MyEnumEnumTypeWrapper):
    pass

HELLO: MyEnum.ValueType  # 0
WORLD: MyEnum.ValueType  # 1
```

`_MyEnumEnumTypeWrapper` extends the EnumTypeWrapper to take/return MyEnum.ValueType rather than int
`MyEnum` is an instance of the `EnumTypeWrapper`.

- Use `_MyEnum` and of metaclass is an implementation detail to make MyEnum.ValueType a valid type w/o a circular dependency
- `V` is supported as an alias of `ValueType` for backward compatibility

### Supports generating type wrappers for fields and maps

M.proto

```proto
message M {
  uint32 user_id = 1 [(mypy_protobuf.options).casttype="mymod.UserId"];
  map<uint32, string> email_by_uid = 2 [
    (mypy_protobuf.options).keytype="path/to/mymod.UserId",
    (mypy_protobuf.options).valuetype="path/to/mymod.Email"
  ];
}
```

mymod.py

```python
UserId = NewType("UserId", int)
Email = NewType("Email", Text)
```

### `py_generic_services`

If `py_generic_services` is set in your proto file, then mypy-protobuf will
generate service stubs. If you want GRPC stubs instead - use the GRPC instructions.

### `readable_stubs`

If `readable_stubs` is set, mypy-protobuf will generate easier-to-read stubs. The downside
to this approach - is that it's possible to generate stubs which do not pass mypy - particularly
in the case of name collisions. mypy-protobuf defaults to generating stubs with fully qualified
imports and mangled global-level identifiers to defend against name collisions between global
identifiers and field names.

If you're ok with this risk, try it out!

```
protoc --python_out=output/location --mypy_out=readable_stubs:output/location
```

### `relax_strict_optional_primitives`

If you are using proto3, then primitives cannot be represented as NULL on the wire -
only as their zero value. By default mypy-protobuf types message constructors to have
non-nullable primitives (eg `int` instead of `Optional[int]`). python-protobuf itself will
internally convert None -> zero value. If you intentionally want to use this behavior,
set this flag! We recommend avoiding this, as it can lead to developer error - confusing
NULL and 0 as distinct on the wire.
However, it may be helpful when migrating existing proto2 code, where the distinction is meaningful

```
protoc --python_out=output/location --mypy_out=relax_strict_optional_primitives:output/location
```

### `use_default_deprecation_warnings`

By default mypy-protobuf will pull the leading and trailing comments from the deprecation option definition,
and insert it into the deprecation warning. This option will instead use a standard deprecation warning instead of comments.

```
protoc --python_out=output/location --mypy_out=use_default_deprecation_warning:output/location
```

### `generate_concrete_servicer_stubs`

By default mypy-protobuf will output servicer stubs with abstract methods. To output concrete stubs, set this option

```
protoc --python_out=output/location --mypy_grpc_out=generate_concrete_servicer_stubs:output/location
```

### `sync_only/async_only`

By default, generated GRPC stubs are compatible with both sync and async variants. If you only
want sync or async GRPC stubs, use this option:

```
protoc --python_out=output/location --mypy_grpc_out=sync_only:output/location
```

or

```
protoc --python_out=output/location --mypy_grpc_out=async_only:output/location
```

### Output suppression

To suppress output, you can run

```
protoc --python_out=output/location --mypy_out=quiet:output/location
```

### GRPC

This plugin provides stubs generation for grpcio generated code.

```
protoc \
    --python_out=output/location \
    --mypy_out=output/location \
    --grpc_out=output/location \
    --mypy_grpc_out=output/location
```

Note that generated code for grpc will work only together with code for python and locations should be the same.
If you need stubs for grpc internal code we suggest using this package https://pypi.org/project/types-grpcio/

#### Async GRPC usage

`mypy-protobuf` generates stubs that are compatible with both sync and async usage, with a few caveats.

In a simple use case, the stubs work as expected.

```python
stub = dummy_Pb2_grpc.DummyServiceStub(grpc.aio.insecure_channel("localhost:1234"))
result = await stub.UnaryUnary(dummy_pb2.DummyRequest(value="cprg"))
typing.assert_type(result, dummy_pb2.DummyReply)
```

If you need to explicitly type something as an async stub (class attr, etc) then you must use deferred annotations, and the async stub, as it does not exist at runtime.

```python
class TestAttribute:
    stub: "dummy_pb2_grpc.DummyServiceAsyncStub"

    def __init__(self) -> None:
        self.stub = dummy_pb2_grpc.DummyServiceStub(grpc.aio.insecure_channel("localhost:1234"))

    async def test(self) -> None:
        result = await self.stub.UnaryUnary(dummy_pb2.DummyRequest(value="cprg"))
        typing.assert_type(result, dummy_pb2.DummyReply)
```

### `_ClearFieldArgType`, `_WhichOneofArgType_<oneof_name>`, `_WhichOneofReturnType_<oneof_name>` and `_HasFieldArgType` aliases

Where applicable, type aliases are generated for the arguments to `ClearField`, `WhichOneof` and `HasField`. These can be used to create typed functions for field manipulation:

```python
  from testproto.edition2024_pb2 import Editions2024Test

  def test_hasfield_alias(msg: Editions2024Test, field: "Editions2024Test._HasFieldArgType") -> bool:
      return msg.HasField(field)

  test_hasfield_alias(Editions2024Test(), "legacy")

  def test_whichoneof_alias(
      msg: SimpleProto3,
      oneof: "SimpleProto3._WhichOneofArgType_a_oneof",
  ) -> "SimpleProto3._WhichOneofReturnType_a_oneof | None":
      return msg.WhichOneof(oneof)

  test_whichoneof_alias(SimpleProto3(), "a_oneof")
```

Note the deferred evaluation (string reference, or `from __future__ import annotations`. This bypasses the fact that the alias does not exist on the runtime class)

### Targeting python2 support

mypy-protobuf's drops support for targeting python2 with version 3.0. If you still need python2 support -

```
python3 -m pip install mypy_protobuf==2.10
protoc --python_out=output/location --mypy_out=output/location
mypy --target-version=2.7 {files}
```

## Releasing to pypi (TODO: Automate)

1. Checkout the commit you need to release
2. Make sure it is tagged with the release version
3. Make sure the working tree is clean
4. Clean dist (`rm -rf dist`)
5. `uv venv`
6. `uv pip install --upgrade build`
7. `uv run python3 -m build`
8. `uv pip install --upgrade twine`
9. `uv run twine upload --repository testpypi dist/*`
10. `uv run twine upload dist/*`

## 3rd Party Tests

3rd Party proto files can be added in `run_tests.sh`, they should be cloned in, generated, type checked, then cleaned up.

## Contributing

Contributions to the implementation are welcome. Please run tests using `./run_test.sh`.
Ensure code is formatted using black.

```
pip3 install black
black .
```

## Contributors

- [@nipunn1313](https://github.com/nipunn1313)
- [@dzbarsky](https://github.com/dzbarsky)
- [@gvanrossum](https://github.com/gvanrossum)
- [@peterlvilim](https://github.com/peterlvilim)
- [@msullivan](https://github.com/msullivan)
- [@bradenaw](https://github.com/bradenaw)
- [@ilevkivskyi](https://github.com/ilevkivskyi)
- [@Ketouem](https://github.com/Ketouem)
- [@nmiculinic](https://github.com/nmiculinic)
- [@onto](https://github.com/onto)
- [@jcppkkk](https://github.com/jcppkkk)
- [@drather19](https://github.com/drather19)
- [@smessmer](https://github.com/smessmer)
- [@pcorpet](https://github.com/pcorpet)
- [@zozoens31](https://github.com/zozoens31)
- [@abhishekrb19](https://github.com/abhishekrb19)
- [@jaens](https://github.com/jaens)
- [@arussellsaw](https://github.com/arussellsaw)
- [@shabbyrobe](https://github.com/shabbyrobe)
- [@reorx](https://github.com/reorx)
- [@zifter](https://github.com/zifter)
- [@juzna](https://github.com/juzna)
- [@mikolajz](https://github.com/mikolajz)
- [@chadrik](https://github.com/chadrik)
- [@EPronovost](https://github.com/EPronovost)
- [@chrislawlor](https://github.com/chrislawlor)
- [@henribru](https://github.com/henribru)
- [@Evgenus](https://github.com/Evgenus)
- [@MHDante](https://github.com/MHDante)
- [@nelfin](https://github.com/nelfin)
- [@alkasm](https://github.com/alkasm)
- [@tarmath](https://github.com/tarmath)
- [@jaredkhan](https://github.com/jaredkhan)
- [@sodul](https://github.com/sodul)
- [@miaachan](https://github.com/miaachan)
- [@Alphadelta14](https://github.com/Alphadelta14)
- [@fergyfresh](https://github.com/fergyfresh)
- [@AlexWaygood](https://github.com/AlexWaygood)
- [@Avasam](https://github.com/Avasam)
- [@artificial-aidan](https://github.com/artificial-aidan)

## Licence etc.

1. License: Apache 2.0.
2. Copyright attribution: Copyright (c) 2022 Nipunn Koorapati