Description
Proposal
I propose to change the type annotation of Env.metadata
from dict[str, Any]
to Mapping[str, Any]
.
Motivation
The purpose of metadata
is to be a read-only store of certain information about envs. As far as I can tell, the attribute is only ever set during class creation and only read afterwards.
This is not what the type annotation dict[str, Any]
communicates, however. In the type hierarchy, it is considered a mutable mapping, meaning that mutating item access is explicitly allowed.
This makes it difficult to define an env when using type checkers or linters. For example, given this piece of code:
from gymnasium import Env
class MyEnv(Env):
metadata = {
"render.modes": ["human", "rgb_array"],
"render_fps": 50,
}
The ruff linter gives this warning:
example.py:5:16: RUF012 Mutable class attributes should be annotated with `typing.ClassVar`
Found 1 error.
The only solution right now is to disable this (generally sensible) warning either globally or for each env individually.
Changing the type annotation is a reasonably small change, should not change any runtime behavior and makes the code communicate better the use cases it anticipates. The only code that would negatively impacted (i.e. receive linter warnings where there were none before) would be code that is already highly suspect, e.g. code that modifies the metadata after class creation.
Pitch
I propose to change the definition of Env.metadata
, which currently looks like this:
from typing import TYPE_CHECKING, Any, Generic, SupportsFloat, TypeVar
...
class Env(Generic[ObsType, ActType]):
...
metadata: dict[str, Any] = {"render_modes": []}
to use the ABC Mapping
:
from typing import TYPE_CHECKING, Any, Generic, Mapping, SupportsFloat, TypeVar
...
class Env(Generic[ObsType, ActType]):
...
metadata: Mapping[str, Any] = {"render_modes": []}
Alternatives
One could also go one step further and declare metadata
a ClassVar[Mapping[str, Any]]
. This would imply that the metadata is only supposed to be defined on the class itself and not e.g. in __init__()
.
However, I haven't clearly thought through the implications that this change would have, especially given that e.g. on Wrapper
, the metadata arguably is an instance rather than class attribute. Some incomplete testing tells me that this would at least be a non-trivial change:
from typing import Any, ClassVar, Mapping
from typing_extensions import reveal_type
class MyEnv:
metadata: ClassVar[Mapping[str, Any]] = {
"render.modes": [],
}
class Wrapper(MyEnv):
@property # Cannot override writeable attribute with read-only property [override]
def metadata(self) -> Mapping[str, Any]:
return MyEnv.metadata
class SubEnv(MyEnv):
metadata: ClassVar[Mapping[str, Any]] = {
"render.modes": ["human", "rgb_array"],
"render_fps": 50,
}
reveal_type(MyEnv.metadata) # Revealed type is "typing.Mapping[builtins.str, Any]"
reveal_type(MyEnv().metadata) # Revealed type is "typing.Mapping[builtins.str, Any]"
reveal_type(Wrapper.metadata) # Revealed type is "def (self: bar.Wrapper) -> typing.Mapping[builtins.str, Any]"
reveal_type(Wrapper().metadata) # Revealed type is "typing.Mapping[builtins.str, Any]"
reveal_type(SubEnv.metadata) # Revealed type is "typing.Mapping[builtins.str, Any]"
reveal_type(SubEnv().metadata) # Revealed type is "typing.Mapping[builtins.str, Any]"
env: MyEnv = Wrapper()
reveal_type(env.metadata) # Revealed type is "typing.Mapping[builtins.str, Any]"
reveal_type(type(env).metadata) # Revealed type is "typing.Mapping[builtins.str, Any]"
Additional context
I use Mypy rather than Pyright for type checking, so it might be worthwhile that changing the annotation from dict
to Mapping
won't break anything for Pyright users.
Checklist
- I have checked that there is no similar issue in the repo