You can use a Protocol with a property that does not have a setter:

class Identifiable(Protocol):
    @property
    def id(self) -> int: ...

Now object.id = 2 fails with a readable mypy error: asd.py:29: error: Property "id" defined in "Identifiable" is read-only

As a side note, it's might not be a good idea to make a variable named id or object, as those are names of built-ins, and it's not clear whether object somewhere in the middle of the code refers to object the built-in class or some variable named object. This is worst with global variables, but I don't consider it good practice with local variables either.

🌐
Python
typing.python.org › en › latest › spec › protocol.html
Protocols — typing documentation
The attributes (variables and methods) of a protocol that are mandatory for another class for it to be assignable to the protocol are called “protocol members”.
🌐
Mypy
mypy.readthedocs.io › en › stable › protocols.html
Protocols and structural subtyping - mypy 1.19.1 documentation
Explicitly including a protocol as a base class is also a way of documenting that your class implements a particular protocol, and it forces mypy to verify that your class implementation is actually compatible with the protocol. In particular, omitting a value for an attribute or a method body will make it implicitly abstract:
🌐
Real Python
realpython.com › python-protocol
Python Protocols: Leveraging Structural Subtyping – Real Python
July 25, 2024 - In Python, a protocol specifies the methods and attributes that a class must implement to be considered of a given type.
🌐
Python
peps.python.org › pep-0544
PEP 544 – Protocols: Structural subtyping (static duck typing) | peps.python.org
The runtime implementation could be done in pure Python without any effects on the core interpreter and standard library except in the typing module, and a minor update to collections.abc: Define class typing.Protocol similar to typing.Generic. Implement functionality to detect whether a class is a protocol or not. Add a class attribute _is_protocol = True if that is the case.
🌐
Python
typing.python.org › en › latest › reference › protocols.html
Protocols and structural subtyping — typing documentation
Explicitly including a protocol as a base class is also a way of documenting that your class implements a particular protocol, and it forces the type checker to verify that your class implementation is actually compatible with the protocol. In particular, omitting a value for an attribute or a method body will make it implicitly abstract:
🌐
Xebia
xebia.com › home › blog › protocols in python: why you need them
Protocols In Python: Why You Need Them | Xebia
July 25, 2022 - What is the main advantage of using Python protocols? Protocols provide a flexible way to perform static type checking without requiring explicit inheritance or registration, offering the benefits of both static and dynamic typing. How do protocols differ from Abstract Base Classes (ABCs)? Unlike ABCs, protocols do not require explicit inheritance. Any class that implements the required methods and attributes ...
Find elsewhere
🌐
Python documentation
docs.python.org › 3 › howto › descriptor.html
Descriptor Guide — Python 3.14.3 documentation
Learning about descriptors not only provides access to a larger toolset, it creates a deeper understanding of how Python works. In general, a descriptor is an attribute value that has one of the methods in the descriptor protocol.
Top answer
1 of 2
37

This is exactly what Protocols are for. In short, Protocols let you use structural instead of nominal subtyping. With nominal subtyping, type A is a subtype of B if A explicitly inherits or extends B. With structural subtyping, type A is a subtype of B if it has the same method and attribute "signatures" as B (with some restrictions).

For example:

# If you're using Python 3.8+
from typing import Protocol

# If you need to support older versions of Python,
# pip-install the 'typing_extensions' module and do:
from typing_extensions import Protocol

class SupportsBarBaz(Protocol):
    bar: int
    baz: int

class MyUnrelatedClass1:
    def __init__(self, bar: int, baz: int) -> None:
        self.bar = bar
        self.baz = baz

class MyUnrelatedClass2:
    def __init__(self, bar: int, baz: int, blah: str) -> None:
        self.bar = bar
        self.baz = baz
        self.blah = blah

class MyUnrelatedClass3:
    def __init__(self, bar: str, baz: str, blah: str) -> None:
        self.bar = bar
        self.baz = baz
        self.blah = blah

def foo(a: SupportsBarBaz) -> int:
    return a.bar + a.baz

# These both type-check, even though there's no explicit relationship
# between 'SupportsBarBaz' and these two classes
foo(MyUnrelatedClass1(1, 2))
foo(MyUnrelatedClass2(1, 2, "abc"))

# But this doesn't type-check, since 'bar' and 'baz' are both strs here
foo(MyUnrelatedClass3("a", "b", "c"))

You can find more information about using Protocols in the mypy docs. The information in that page is all compliant with the PEP, so the info there should all apply to other type checkers, assuming they've finished implementing their own support for Protocols.

You can also find slightly more complex examples of using Protocols in typeshed, the repository of type hints for the Python standard library.

Though, I suppose this all matters only if you actually intend on using static analysis in your code. If not, you could maybe do something simpler and just define a custom type alias to Any, document what that alias is "supposed" to mean, and use that alias instead of a full-fledged protocol. That alias would be almost completely useless for the purposes of static analysis/autocompletion tools/etc, but humans generally have no issues reading comments.

2 of 2
1

Type hints can only refer to a class, so create an abstract class

import abc

class MyType(abc.ABC):

    @abc.abstractproperty
    def foo(self):
        pass

    @abc.abstractproperty
    def bar(self):
        pass

And declare f(a: MyType)

🌐
Python Tutorial
pythontutorial.net › home › python oop › python protocol
Python Protocol
March 31, 2025 - First, define an Item class that inherits from the Protocol with two attributes: quantity and price: class Item(Protocol): quantity: float price: floatCode language: Python (python)
🌐
Towards Data Science
towardsdatascience.com › home › latest › protocols in python
Protocols in Python | Towards Data Science
January 21, 2025 - Thus, Python 3.8 introduced protocols, alleviating aforementioned issues. Protocols, as the name suggests, work implicitly by defining an "interface" of expected attributes / methods, and checking whether the classes in question provide these, if necessary:
🌐
Medium
medium.com › @AlexanderObregon › how-pythons-descriptor-protocol-implements-attribute-access-4eec5edc62d5
How Python’s Descriptor Protocol Implements Attribute Access
December 26, 2024 - The descriptor protocol is a fundamental mechanism in Python that governs how attribute access — retrieval, assignment, and deletion — is handled. It operates through a set of specially defined methods that are implemented in a class, enabling ...
🌐
Andrewbrookins
andrewbrookins.com › technology › building-implicit-interfaces-in-python-with-protocol-classes
Building Implicit Interfaces in Python with Protocol Classes – Andrew Brookins
So, why is this called a “protocol” and what kinds of things is it good for? Let’s go deeper to answer those questions. The built-in function len() works with any object that has a __len__() method. Objects don’t have to declare that they have a __len__() method or subclass any special classes to work with len(). Python programmers call this state of affairs a protocol, and the most common example is probably the iteration protocol.
🌐
GitHub
github.com › python › typing › discussions › 1123
Read-only `Protocol` attributes (again) · python/typing · Discussion #1123
In my last question, I asked about how to mark a protocol attribute as read-only in order to accept either an object with a read-only attribute or a writable attribute. I have a similar question, but slightly different: How do I mark a Protocol attribute as read-only so that any object that the operation obj.prop is valid can be passed in.
Author   python
🌐
Python Reference
python-reference.readthedocs.io › en › latest › docs › dunderdsc
Descriptor Protocol — Python Reference (The Right Way) 0.1 documentation
In general, a descriptor is an object attribute with “binding behavior”, one whose attribute access has been overridden by methods in the descriptor protocol: __get__(), __set__(), and __delete__().
🌐
YouTube
youtube.com › watch
You NEED to know about Python protocols - YouTube
Protocols are perfect for attribute and method checking, and really shine in situations where you need a lazy check to ensure an object has everything it nee...
Published   November 18, 2024
🌐
Real Python
realpython.com › ref › glossary › protocol
protocol | Python Glossary – Real Python
A set of methods and attributes that a class must implement to support specific behavior or functionality.
🌐
Turingtaco
turingtaco.com › protocols-default-methods-inheritance-and-more
Protocols: Default Methods, Inheritance, and More
December 7, 2024 - First and foremost, Protocols follow the conventional inheritance rules found in Python. This means a class that inherits from another will inherit its attributes and methods.
🌐
Devzery
devzery.com › post › mastering-python-protocols-a-comprehensive-guide
Mastering Python Protocols: A Comprehensive Guide
August 27, 2024 - Lazy attributes are only computed when accessed, which can be implemented using the descriptor protocol. This is particularly useful for attributes that are costly to compute or that may not always be needed. Python protocols, especially the descriptor protocol, provide a powerful mechanism for customizing and controlling object behavior.