Discussion:
Any way to "subclass" typing.Annotated?
Add Reply
Ian Pilcher
2025-01-28 22:02:47 UTC
Reply
Permalink
(Note: I have mail delivery disabled for this list and read it through
GMane. Please copy me on any responses, so that I can respond with
proper threading.)

From the things that I probably shouldn't spend my free time on
department ...

As background, I'm working on a project that is going to involve a bunch
of abstract classes and dynamic types, and I've found that Python's
existing abstract class implementation leaves a lot to be desired,
particularly the inability to create abstract class variables and class
methods. Having been seduced by the Siren song of Python's flexibility,
I've been rolling my own implementation.

Now to my question.

I'm currently using annotations to create abstract class variables, for
example:

class Foo(object, metaclass=AbstractType):

acv: Annotated[int, abstract]

('abstract' is simply a unique "flag" object.)

This works just fine, but it's somewhat un-idiomatic. What I'd like to
be able to do is create my own type, so that I could do something like
this:

class Foo(object, metaclass=AbstractType):

acv: AbstractClassVariable[int]

Essentially I'd like to create "subclass" of typing.Annotated that
always sets the metadata to 'abstract'. Thus far, I haven't found a
way to do this, as typing.Annotated can't be subclassed.

Anyone have any ideas?
--
========================================================================
If your user interface is intuitive in retrospect ... it isn't intuitive
========================================================================
Stefan Ram
2025-01-29 13:03:55 UTC
Reply
Permalink
Post by Ian Pilcher
Essentially I'd like to create "subclass" of typing.Annotated that
always sets the metadata to 'abstract'. Thus far, I haven't found a
way to do this, as typing.Annotated can't be subclassed.
Alright, so here's the deal: you're right that typing.Annotated
is kind of stubborn and won't let you subclass it. But don't
worry, there's a way to finesse this. You can rig up something
that works just as well without having to wrestle Python's
type system into submission. Let me walk you through it.

Option 1: Factory Function for AbstractClassVariable

Think of this as building a little factory that cranks out Annotated
types with your custom abstract flag baked in. Here's how it looks:

Python

from typing import Annotated, TypeVar, Any

# This is your "abstract" flag. It's just a unique object.
abstract = object()

# A generic type variable to keep things flexible.
T = TypeVar("T")

# The factory function
def AbstractClassVariable(type_: T) -> Any:
return Annotated[type_, abstract]

Now, when you're writing your class, you just call this factory
like so:

Python

class Foo(object, metaclass=AbstractType):
acv: AbstractClassVariable[int]

Boom. Clean, simple, and gets the job done without any drama.

Option 2: Custom Wrapper Class

If you're more into the idea of having something that looks
like a class but still does the same thing, you can roll your
own wrapper class. Here's what that might look like:

Python

from typing import Annotated, TypeVar, Generic

# Again, your trusty "abstract" flag.
abstract = object()

# Same type variable as before.
T = TypeVar("T")

class AbstractClassVariable(Generic[T]):
def __class_getitem__(cls, item: T) -> Any:
return Annotated[item, abstract]

With this setup, you can write your class exactly the way you
wanted to in the first place:

Python

class Foo(object, metaclass=AbstractType):
acv: AbstractClassVariable[int]

It's basically the same as Option 1 but with a bit more flair.
What's Going On Here?

- Factory Function: This is just a shortcut to make sure
every time you create an abstract class variable, it
automatically comes with your abstract flag attached.

- Custom Wrapper Class: By overriding __class_getitem__,
we're letting Python treat AbstractClassVariable[int] as
shorthand for Annotated[int, abstract]. It's like hacking
the system without actually breaking anything.

Both options are solid—pick whichever one vibes better with your
style.

Enforcing Abstract Class Variables

Now, if you want to make sure subclasses actually define
these abstract class variables (because let's face it,
someone will forget), you'll need to tweak your metaclass
a bit. Here's an example:

Python

class AbstractType(type):
def __init__(cls, name, bases, namespace):
super().__init__(name, bases, namespace)
for attr_name, attr_type in namespace.get("__annotations__", {}).items():
if isinstance(attr_type, Annotated) and abstract in getattr(attr_type, "__metadata__", []):
if not hasattr(cls, attr_name):
raise TypeError(f"Class {cls.__name__} must define abstract class variable '{attr_name}'.")

This basically scans through all the annotated variables in
your class and checks if they've got the abstract flag. If they
do and no one bothered to define them in a subclass? Game over.

Example in Action

Here's how it plays out:

Python

class Foo(object, metaclass=AbstractType):
acv: AbstractClassVariable[int]

# This will blow up because Bar doesn't define 'acv'.
class Bar(Foo):
pass

# This works because Baz actually defines 'acv'.
class Baz(Foo):
acv = 42

So yeah, that's the gist. It's flexible enough to fit into
whatever setup you've got going on and keeps things Pythonic
without veering off into uncharted territory.

Let me know if anything feels off or if I missed something!
Stefan Ram
2025-01-29 13:34:51 UTC
Reply
Permalink
Post by Stefan Ram
Alright, so here's the deal: you're right that typing.Annotated
Alright, so here's the deal with "typing.Annotated" in Python.

It's like adding extra toppings to your In-N-Out burger - you're
not just saying what type of meat you want, you're throwing on
some secret sauce too. Imagine you're at a Coachella for coders.

Here's what typing.Annotated brings to the party:

- Type Declaration: You're still telling Python what kind of
data you're working with, like saying "this variable is as
integer as the number of surfers at Huntington Beach."

- Extra Info: But now you can slap on some bonus details,
like how you'd stick a "I climbed Half Dome" sticker on
your Nalgene.

- Flexibility: This extra info can be pretty much anything -
like how you can put literally anything on avocado toast
and Californians will eat it up.

Here's a quick example, as easy as parallel parking on Lombard
Street:

Python

from typing import Annotated

# Plain Jane version
age: int

# Fancy pants version
age: Annotated[int, "How many times you've seen the Hollywood sign"]

Both lines are saying age is an integer, but the Annotated one
is like adding a little note that your hipster friend's artisanal
documentation tool might dig.

Just a heads up, you might need to grab the typing_extensions
module (like picking up some craft IPA from your local microbrewery)
to use Annotated in older Python versions. It's fresher than
the produce at Berkeley Bowl in the newer versions.

Bottom line, typing.Annotated is like the GPS for your code
- it doesn't change where you're going, but it sure makes the
journey clearer for everyone involved. It's especially clutch
when you're working on projects bigger than Silicon Valley egos.
Stefan Ram
2025-01-29 13:45:30 UTC
Reply
Permalink
Post by Stefan Ram
# The factory function
return Annotated[type_, abstract]
Alright, so now I've got something here that actually compiles!

Python

from typing import Annotated, TypeVar, Any, Generic

# This is your "abstract" flag. It's just a unique object.
abstract = object()

# A generic type variable to keep things flexible.
T = TypeVar("T")

# Define AbstractClassVariable as a class instead of a function
class AbstractClassVariable(Generic[T]):
def __class_getitem__(cls, item):
return Annotated[item, abstract]

class AbstractType(type):
def __new__(cls, name, bases, namespace):
for key, value in namespace.items():
if isinstance(value, type(Annotated)):
if abstract in value.__metadata__:
raise TypeError(f"Abstract class variable '{key}' must be defined")

return super().__new__(cls, name, bases, namespace)

class Foo(object, metaclass=AbstractType):
acv: AbstractClassVariable[int]
Stefan Ram
2025-01-29 13:48:00 UTC
Reply
Permalink
Post by Stefan Ram
Option 2: Custom Wrapper Class
To compile, this should read:

Python

from typing import Annotated, TypeVar, Generic, Any

# Your trusty "abstract" flag, chillin' like it's at Venice Beach
abstract = object()

# Type variable, as constant as the NorCal fog
T = TypeVar("T")

class AbstractClassVariable(Generic[T]):
def __class_getitem__(cls, item: T) -> Any:
return Annotated[item, abstract]
Fabien LUCE
2025-01-31 10:41:22 UTC
Reply
Permalink
Maybe you'd better use descriptors?

On Tue, 28 Jan 2025 at 23:03, Ian Pilcher via Python-list <
Post by Ian Pilcher
(Note: I have mail delivery disabled for this list and read it through
GMane. Please copy me on any responses, so that I can respond with
proper threading.)
From the things that I probably shouldn't spend my free time on
department ...
As background, I'm working on a project that is going to involve a bunch
of abstract classes and dynamic types, and I've found that Python's
existing abstract class implementation leaves a lot to be desired,
particularly the inability to create abstract class variables and class
methods. Having been seduced by the Siren song of Python's flexibility,
I've been rolling my own implementation.
Now to my question.
I'm currently using annotations to create abstract class variables, for
acv: Annotated[int, abstract]
('abstract' is simply a unique "flag" object.)
This works just fine, but it's somewhat un-idiomatic. What I'd like to
be able to do is create my own type, so that I could do something like
acv: AbstractClassVariable[int]
Essentially I'd like to create "subclass" of typing.Annotated that
always sets the metadata to 'abstract'. Thus far, I haven't found a
way to do this, as typing.Annotated can't be subclassed.
Anyone have any ideas?
--
========================================================================
If your user interface is intuitive in retrospect ... it isn't intuitive
========================================================================
--
https://mail.python.org/mailman/listinfo/python-list
Loading...