Empresas
Empleos
  • Sobre nosotros
  • Soluciones
    • Publicación de vacantes
      Publica tu vacante y recibe candidatos calificados en 48h.
    • Evaluación de candidatos
      500+ pruebas técnicas y psicológicas, más anti-fraude.
    • Headhunting
      Búsqueda ejecutiva a la medida de principio a fin.
    • Nómina + EOR
      Dispersión de nómina y EOR en más de 15 países de LATAM.
  • Precios
  • Empleos

0

236
Vistas
Why do keyword arguments to a class definition reappear after they were removed?

I created a metaclass that defines the __prepare__ method, which is supposed to consume a specific keyword in the class definition, like this:

class M(type):
    @classmethod
    def __prepare__(metaclass, name, bases, **kwds):
        print('in M.__prepare__:')
        print(f'  {metaclass=}\n  {name=}\n'
              f'  {bases=}\n  {kwds=}\n  {id(kwds)=}')
        if 'for_prepare' not in kwds:
            return super().__prepare__(name, bases, **kwds)
        arg = kwds.pop('for_prepare')
        print(f'  arg popped for prepare: {arg}')
        print(f'  end of prepare: {kwds=} {id(kwds)=}')
        return super().__prepare__(name, bases, **kwds)

    def __new__(metaclass, name, bases, ns, **kwds):
        print('in M.__new__:')
        print(f'  {metaclass=}\n  {name=}\n'
              f'  {bases=}\n  {ns=}\n  {kwds=}\n  {id(kwds)=}')
        return super().__new__(metaclass, name, bases, ns, **kwds)


class A(metaclass=M, for_prepare='xyz'):
    pass

When I run it, the for_prepare keyword argument in the definition of class A reappears in __new__ (and later in __init_subclass__, where it causes an error):

$ python3 ./weird_prepare.py
in M.__prepare__:
  metaclass=<class '__main__.M'>
  name='A'
  bases=()
  kwds={'for_prepare': 'xyz'}
  id(kwds)=140128409916224
  arg popped for prepare: xyz
  end of prepare: kwds={} id(kwds)=140128409916224
in M.__new__:
  metaclass=<class '__main__.M'>
  name='A'
  bases=()
  ns={'__module__': '__main__', '__qualname__': 'A'}
  kwds={'for_prepare': 'xyz'}
  id(kwds)=140128409916224
Traceback (most recent call last):
  File "./weird_prepare.py", line 21, in <module>
    class A(metaclass=M, for_prepare='xyz'):
  File "./weird_prepare.py", line 18, in __new__
    return super().__new__(metaclass, name, bases, ns, **kwds)
TypeError: __init_subclass__() takes no keyword arguments

As you can see the for_prepare item is removed from the dict, and the dict that is passed to __new__ is the same object that was passed to __prepare__ and the same object that the for_prepare item was popped from, but in __new__ it reappeared! Why does a keyword that was deleted from the dict get added back in?

over 4 years ago · Santiago Trujillo
3 Respuestas
Responde la pregunta

0

and the dict that is passed to new is the same object that was passed to prepare

Unfortunately, this is where you are wrong.

Python only recycles the same object id.

If you create a new dict inside __prepare__ you will notice the id of kwds changes in __new__.

class M(type):
    @classmethod
    def __prepare__(metaclass, name, bases, **kwds):
        print('in M.__prepare__:')
        print(f'  {metaclass=}\n  {name=}\n'
              f'  {bases=}\n  {kwds=}\n  {id(kwds)=}')
        if 'for_prepare' not in kwds:
            return super().__prepare__(name, bases, **kwds)
        arg = kwds.pop('for_prepare')
        x = {} # <<< create a new dict
        print(f'  arg popped for prepare: {arg}')
        print(f'  end of prepare: {kwds=} {id(kwds)=}')
        return super().__prepare__(name, bases, **kwds)

    def __new__(metaclass, name, bases, ns, **kwds):
        print('in M.__new__:')
        print(f'  {metaclass=}\n  {name=}\n'
              f'  {bases=}\n  {ns=}\n  {kwds=}\n  {id(kwds)=}')
        return super().__new__(metaclass, name, bases, ns, **kwds)


class A(metaclass=M, for_prepare='xyz'):
    pass

Output:

in M.__prepare__:
  metaclass=<class '__main__.M'>
  name='A'
  bases=()
  kwds={'for_prepare': 'xyz'}
  id(kwds)=2595838763072
  arg popped for prepare: xyz
  end of prepare: kwds={} id(kwds)=2595838763072
in M.__new__:
  metaclass=<class '__main__.M'>
  name='A'
  bases=()
  ns={'__module__': '__main__', '__qualname__': 'A'}
  kwds={'for_prepare': 'xyz'}
  id(kwds)=2595836298496 # <<< id has changed now
Traceback (most recent call last):
  File "d:\nemetris\mpf\mpf.test\test_so4.py", line 22, in <module>
    class A(metaclass=M, for_prepare='xyz'):
  File "d:\nemetris\mpf\mpf.test\test_so4.py", line 19, in __new__ 
    return super().__new__(metaclass, name, bases, ns, **kwds)     
TypeError: A.__init_subclass__() takes no keyword arguments        
over 4 years ago · Santiago Trujillo Denunciar

0

  • don't send **kwds to __new__, it won't catch them after python 3.6 .

example

class M(type):
    @classmethod
    def __prepare__(metaclass, name, bases, **kwds):
        # print('in M.__prepare__:')
        # print(f'  {metaclass}=\n  {name}=\n'
        #       f'  {bases}=\n  {kwds}=\n  {id(kwds)}=')
        if 'for_prepare' not in kwds:
            return super().__prepare__(name, bases, **kwds)
        # arg = kwds.pop('for_prepare')
        # print(f'  arg popped for prepare: {arg}')
        # print(f'  end of prepare: {kwds}= {id(kwds)}=')
        return super().__prepare__(name, bases, **kwds)

    def __new__(metaclass, name, bases, ns, **kwds):
        print('in M.__new__:')
        print(f'  metaclass = {metaclass}\n  name = {name}\n'
              f'  bases = {bases}\n  ns = {ns}\n  kwds = {kwds}\n  id_kwds = {id(kwds)}')
        return super().__new__(metaclass, name, bases, ns)


class A(metaclass=M, for_prepare='xyz'):
    pass

a = A()

result:

in M.__new__:
  metaclass = <class '__main__.M'>
  name = A
  bases = ()
  ns = {'__module__': '__main__', '__qualname__': 'A'}
  kwds = {'for_prepare': 'xyz'}
  id_kwds = 2101285477256
over 4 years ago · Santiago Trujillo Denunciar

0

This is not an effect of metaclasses, but of **kwargs. Whenever a function is called with **kwargs, the current dict is unpacked and not passed on. Whenever a function receives **kwargs, a new dict is created.

In effect, when both of caller/callee use **kwargs then the dict seen by either is a copy.

Compare the setup of using **kwargs in isolation:

def first(**kwargs):
    print(f"Popped 'some_arg': {kwargs.pop('some_arg')!r}")

def second(**kwargs):
    print(f"Got {kwargs} in the end")

def head(**kwargs):
    first(**kwargs)
    second(**kwargs)

head(a=2, b=3, some_arg="Watch this!", c=4)
# Popped 'some_arg': 'Watch this!'
# Got {'a': 2, 'b': 3, 'some_arg': 'Watch this!', 'c': 4} in the end

Likewise, __prepare__ and __new__ are separately called when creating a class. Their **kwargs are shallow copies and neither adding nor removing items is visible to the other call.

over 4 years ago · Santiago Trujillo Denunciar
Responde la pregunta
Encuentra empleos remotos

¡Descubre la nueva forma de encontrar empleo!

Top de empleos
Top categorías de empleo
Empresas
Publicar vacante Precios Comercial
Legal
Términos y condiciones Política de privacidad
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Recomiéndame algunas ofertas
Necesito ayuda