How to Put Keyword Arguments in your Python Class Definitions
Tags: python, programming, metaprogramming, metaclass
Published: 2022-01-15 12:40:03

I have recently looked at using a library called `SQLModel`. `SQLModel` is an ORM being developed by the same guy behind `FastAPI`, Sebastián Ramírez. This library combines `pydantic` and `SQLAlchemy` to give users a new and hopefully better way to define their Models.

This is an example model that inherits from `SQLModel`.

```python
from sqlmodel import SQLModel, Field

class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True) 
    name: str 
    secret_name: str 
    age: int | None
```

Not gonna lie, defining database models the same way you would a dataclass sounds cool. Now, take a closer look at this syntax `table=True` beside the class definition. This technique is new to me (and off the record guys, I had to read through some `SQLALchemy` codes for a few hours simply because I explicitly forgot to put it in my model).

When you think about it, ORM models are definitely one of the places where it makes sense to put *keyword arguments* in the class definition itself. But how would one do it? How would someone prompt users to put a *keyword argument* in their class definitions?

## Using a Metaclass

As the saying goes, if you have to ask why you would need a *metaclass*, then you probably don't need it. But now that we have a use case, let's have a discussion.

Refer to the code below:

```python
class StudentMeta(type):
    def __new__(cls, *args, **kwargs):
        print(f"{args = }")
        print(f"{kwargs = }")
        return super().__new__(cls, *args)

class Student(metaclass=StudentMeta, table=True):
    ...

troy = Student()
```

`StudentMeta` is our metaclass, `Student` is our class, and `troy` is a `Student` object.

Below is the output of the code:

```python
args = ('Student', (), {'__module__': '__main__', '__qualname__': 'Student'})
kwargs = {'table': True}
```

Note that `args` is a tuple with 3 elements. Traditionally, these are called `name`, `bases`, and `dict` (not to be confused with the *dictionary* built-in).

You may go further and run the following:

```python
print(f"{troy.__class__.__name__ = }")
print(f"{troy.__class__.__bases__ = }")
print(f"{troy.__class__.__dict__ = }")

# Output should be:
# troy.__class__.__name__ = 'Student'
# troy.__class__.__bases__ = (<class 'object'>,)
# troy.__class__.__dict__ = mappingproxy({'__module__': '__main__', '__dict__': <attribute '__dict__' of 'Student' objects>, '__weakref__': <attribute '__weakref__' of 'Student' objects>,'__doc__': None})
```

These *args* are the required arguments for when constructing a new type.

What's happening here is that similar to how `42` is an object of type `int`, and `int` *is a class of* `type`...

- `troy` is an object of type `Student`;
- `Student` *is a class of* type `StudentMeta`;
- `StudentMeta` *is a class of* `type`.

`StudentMeta` is undergoing a normal type creation, except that we have it modified to print all *args* and *kwargs* used in the instantiation. Take note that the return value of `__new__` is `return super().__new__(cls, *args)`. This is because `type(...)` require only the `name`, `bases`, and `dict` to create a new type.

That leaves us with *kwargs*. Do note that `print(f"{kwargs = }")` was triggered when `Student` was *defined*.


## Using \_\_init_\_subclass_\_

`__init__subclass__` use subclassing to modify the behavior of a Parent's subclass. 

```python
class Base:
    def __init_subclass__(cls, *args, **kwargs) -> None:
        print(f"{args = }")
        print(f"{kwargs = }")
        super().__init_subclass__()

class Student(Base, table=True):
    ...

abed = Student()
```

In the example above,

- `abed` is an object of type `Student`;
- `Student` *is a class of* `type`;
- `Base` *is a class of* `type`.

And the output is below:

```python
args = ()
kwargs = {'table': True}
```

There are no *args* because we did not have to create a new `type`; `__init__subclass__` only modifies the creation of a new child class (which is what we did in the earlier example). Note that `__init__subclass__` does not have a `return` statement.

Not only that but ...

```python
print(f"{abed.__class__.__name__ = }")
print(f"{abed.__class__.__bases__ = }")
print(f"{abed.__class__.__dict__ = }")

# Output should be:
# abed.__class__.__name__ = 'Student'
# abed.__class__.__bases__ = (<class '__main__.Base'>,)
# abed.__class__.__dict__ = mappingproxy({'__module__': '__main__', '__doc__': None})
```

## Putting it all together

In this article, we covered two ways to use *keyword arguments* in your class definitions. Metaclasses offer a way to modify the type creation of classes. A simpler way would be to use `__init__subclass__` which modifies only the behavior of the child class' creation.

Regardless of the method, these *keyword arguments* can only be used <u>*during the creation*</u> of a class.