mypy-protobuf: Generate mypy stub files from protobuf specs
2.10 is the last version of mypy-protobuf which supports targeting python 2.7.
Built originally with love at Dropbox
See Changelog for recent changes.
Earlier releases might work, but aren't tested
Earlier releases might work, but aren't tested
Earlier releases might work, but aren't tested
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.
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
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
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
See Changelog for full listing
--pyi_out) and mypy-protobuf(As of 11/17/2025)
mypy-protobuf generates stubs for GRPC services and clientsmypy-protobuf generates correctly typed HasField methods depending on field presence, pyi_out does not type HasField argumentsmypy-protobuf generates correctly typed constructors dependinding on field presence.mypy-protobuf generates correctly typed HasField, WhichOneof, and ClearField methods.mypy-protobuf and pyi_out generate enums. See this issue for detailsHasField, WhichOneof and ClearField argumentsmypy-protobuf marks enums, enum values, messages, message fields, services, and methods with @warnings.deprecated if the deprecation option is set to true.mypy-protobuf:
"""
@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:
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: ...
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.
Enum int values produce stubs which wrap the int values in NewType
enum MyEnum {
HELLO = 0;
WORLD = 1;
}
Will yield an enum type wrapper 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
# 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 includes ValueType
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
from typing import Tuple, TYPE_CHECKING
HELLO = Tuple['MyEnum.ValueType', 'MyEnum.ValueType']
if TYPE_CHECKING:
HELLO = Tuple[MyEnum.ValueType, MyEnum.ValueType]
mypy-protobuf autogenerates an instance of the EnumTypeWrapper as follows.
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.
_MyEnum and of metaclass is an implementation detail to make MyEnum.ValueType a valid type w/o a circular dependencyV is supported as an alias of ValueType for backward compatibilityM.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
UserId = NewType("UserId", int)
Email = NewType("Email", Text)
py_generic_servicesIf 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_stubsIf 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_primitivesIf 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_warningsBy 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_stubsBy 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_onlyBy 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
To suppress output, you can run
protoc --python_out=output/location --mypy_out=quiet:output/location
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/
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.
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.
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 aliasesWhere applicable, type aliases are generated for the arguments to ClearField, WhichOneof and HasField. These can be used to create typed functions for field manipulation:
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)
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}
rm -rf dist)uv venvuv pip install --upgrade builduv run python3 -m builduv pip install --upgrade twineuv run twine upload --repository testpypi dist/*uv run twine upload dist/*3rd Party proto files can be added in run_tests.sh, they should be cloned in, generated, type checked, then cleaned up.
Contributions to the implementation are welcome. Please run tests using ./run_test.sh. Ensure code is formatted using black.
pip3 install black
black .