Skip to content

[Proposal] Change type of metadata from dict to Mapping #951

Open
@troiganto

Description

@troiganto

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions