Python functions can take a variable keyword parameter, often known as **kwargs
.
def myfunc(**kwargs):
print(kwargs)
> myfunc(a=1, b=2)
{'a': 1, 'b': 2}
Sometimes you want to specify precisely which keyword arguments exist.
class User:
def __init__(self, email_address=None, password=None, first_name=None, last_name=None):
self.email_address = email_address
self.password = password
self.first_name = first_name
self.last_name = last_name
def __repr__(self):
name = f'{type(self).__name__}'
attrs = ', '.join([
f'{k}={v}' for k, v in self.__dict__.items()
if not k.startswith('_')
])
return f"<{name}: {attrs}>"
def create_user(*, email_address, password, first_name=None, last_name=None):
return User(
email_address=email_address,
password=password,
first_name=first_name,
last_name=last_name,
)
> user = create_user(
email_address='[email protected]',
password='dummy',
first_name='Devin',
last_name='Fee',
)
> print(user)
<User: email_address=None, password=None, first_name=None, last_name=None>
Which works great, until you want to distinguish between a provided keyword argument, and a possible keyword argument. This requires a marker, e.g. Missing
.
class _Missing:
pass
Missing = _Missing()
provided = lambda **kwargs: {k: v for k, v in kwargs.items() if v is not Missing}
def update_user(user, *, email_address=Missing, password=Missing, first_name=Missing, last_name=Missing):
kwargs = provided(
email_address=email_address,
password=password,
first_name=first_name,
last_name=last_name,
)
for k, v in kwargs.items():
setattr(user, k, v)
return user
> user = update_user(user, password='seekrit')
> print(user)
<User: email_address=me@email.com, password=seekrit, first_name=Devin, last_name=Fee>
But what if you have many objects like User
, and you want a base class to provide create / update functionality? Also, while the keyword arguments are great for documenation, they are tedious to write out in the signature of a function and then recombine in the function itself.
This is what manifest_kw
does for you - it converts the variable keyword argument (often, **kwargs
) into explicit required
, optional
, and default
keyword only arguments.
Still want to provide **extra_kwargs
? Just supply var_keywords=True
to manifest_kw()
.
class BaseService:
__model__ = None
def create(self, **kwargs):
# pylint: disable=E1102, not-callable
return self.__model__(**kwargs)
class UserService(BaseService):
__model__ = User
@manifest_vk(
required=('email_address', 'password'),
optional=('first_name', 'last_name'),
)
def create(self, **kwargs):
return super().create(**kwargs)
def create2(
self,
*,
email_address,
password,
first_name=Missing,
last_name=Missing,
):
return super().create(**provided(
email_address=email_address,
password=password,
first_name=first_name,
last_name=last_name,
))
import inspect
assert inspect.signature(UserService.create) == inspect.signature(UserService.create2)
This is beneficial for three reasons:
- it simplifies documentation and discoverability
- it reduces the amount of boilerplate needed to be explicit
- it guards against unwanted keyword arguments and allows providing defaults.
For the third point, consider the case where changing the email_address
is a special revision and has side effects that are important to manage separately:
class BaseService:
__model__ = None
def create(self, **kwargs):
# pylint: disable=E1102, not-callable
return self.__model__(**kwargs)
class UserService(BaseService):
__model__ = User
@manifest_vk(
required=('email_address', 'password'),
optional=('first_name', 'last_name'),
)
def create(self, **kwargs):
return super().create(**kwargs)
@manifest_vk(
optional=('first_name', 'last_name', 'password'),
)
def update(self, user, **kwargs):
# no email_address here!
for k, v in kwargs.items():
setattr(user, k, v)
return user
def change_email_address(self, user, email_address):
user.email_address = email_address
send_confirmation_email()
return user