Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NamedTuple can't inherit from another class #116241

Open
Conchylicultor opened this issue Mar 2, 2024 · 6 comments
Open

NamedTuple can't inherit from another class #116241

Conchylicultor opened this issue Mar 2, 2024 · 6 comments
Labels
stdlib Python modules in the Lib dir topic-typing type-feature A feature request or enhancement

Comments

@Conchylicultor
Copy link
Contributor

Conchylicultor commented Mar 2, 2024

Feature or enhancement

Proposal:

Currently this code is failing:

class A:
  pass

class B(A, typing.NamedTuple):
  x: int

Raising:

TypeError: can only inherit from a NamedTuple type and Generic

But this code is not:

class A:
  pass

class _B(typing.NamedTuple):
  x: int

class B(A, _B):
  pass

B(x=1)

I believe those 2 snippets are mostly equivalent (the final B class behave the same), so it's confusing and force boilerplate that class B(A, typing.NamedTuple): fail.

Has this already been discussed elsewhere?

This is a minor feature, which does not need previous discussion elsewhere

Links to previous discussion of this feature:

No response

Linked PRs

@Conchylicultor Conchylicultor added the type-feature A feature request or enhancement label Mar 2, 2024
@sunmy2019 sunmy2019 added topic-typing stdlib Python modules in the Lib dir labels Mar 2, 2024
@AlexWaygood
Copy link
Member

We previously discussed this as something some users might want in #88089 (comment). At that point in time, we decided not to allow multiple inheritance with arbitrary classes, just with typing.Generic, because:

  • Nobody had asked for it yet/we weren't sure if there was a common need for it
  • It would probably be tricky for type checkers to understand such constructs; NamedTuple is heavily special-cased by type checkers as it is. (Note that I don't consider this a particularly strong argument: the runtime should generally leave it to type checkers to disallow constructs they don't support. It's not the runtime's job to disallow constructs that type checkers don't understand.)
  • There might be corner cases at runtime where this would cause unexpected/unintuitive behaviour; we'd have to think through it carefully.

If there's a need for using mixin classes with NamedTuple, I'm open to revisiting this restriction.

@AlexWaygood
Copy link
Member

One immediate issue I see is what to do about __slots__. Currently all namedtuple classes have __slots__ set to the empty tuple, which means that instances of namedtuple classes use less memory than instances of classes that don't use __slots__. But setting __slots__ to the empty tuple is useless unless all base classes also have __slots__ set to the empty tuple. What if somebody tries to use a mixin class like A in @Conchylicultor's example, that doesn't set __slots__? Should we raise an exception, on the grounds that this namedtuple class won't come with the memory savings you might expect from using a namedtuple? Or should we assume the user knows what they're doing here, and let it pass?

@serhiy-storchaka
Copy link
Member

#31781 was proposed with implementation of this feature, but it did not receive good support and more limited version was accepted instead. See also a discussion in #88089.

@Conchylicultor
Copy link
Contributor Author

Thanks for the answer, I understand.

I don't really have answer on the various points you raised. But I can provide more context on my use-case.

In our codebase, we're defining some protocol, to handle saving various object types (Dataset, Model,...):

class Checkpointable(abc.ABC):

  @abc.abstractmethod
  def __kd_restore__(self, x):
    ...


class Dataset(Checkpointable):
  ...

class State(Checkpointable):
  ...


def restore[T: Checkpointable](path: PathLike, obj: T) -> T:
  ...

To save multiple object at once, I was trying to add some wrapper:

class TopLevel(typing.NamedTuple, Checkpointable):
  state: State
  ds: Dataset

The reason I was trying to use named tuple rather than dataclasses is because it allow a much more compact syntax in this specific case:

state, ds = restore('/tmp/', TopLevel(state, ds))

vs

out = restore('/tmp/', TopLevel(state, ds))
state = out.state
ds = out.ds

My actual use-case is more complicated with more indirections, variables. But that's the main idea

@serhiy-storchaka
Copy link
Member

I updated #31781. It is just removing 4 lines of code which forbid multiple inheritance.

@serhiy-storchaka
Copy link
Member

The currently working workaround is to use an immediate class:

class TopLevel(collections.namedtuple('TopLevel', ('state', 'ds')), Checkpointable):
  pass

or

class TopLevel(typing.NamedTuple):
  state: State
  ds: Dataset
class TopLevel(TopLevel, Checkpointable):
  pass

Is such complication necessary or we can omit an immediate step?

The issue with __slots__ is solved in the same way as in other cases of using __slots__ or named tuples: you should add __slots__ = () in all base classes if you want to get a benefit from __slots__ in child classes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stdlib Python modules in the Lib dir topic-typing type-feature A feature request or enhancement
Projects
None yet
Development

No branches or pull requests

4 participants