Pythonのコードに出てくるclsは、@classmethodを付けたメソッドの第一引数に自動で渡される「クラス自身」です。インスタンスを受け取るselfと位置は同じで、中身がクラスである点だけが違います。この記事では、clsとselfの違い、staticmethodとの使い分け、typing.Selfの型ヒント、クラス変数の落とし穴、Python 3.13で削除された書き方までを、3.10〜3.15.0で実行した結果で説明します。
まとめ:clsと@classmethodの要点
clsは@classmethodの第一引数に入るクラスオブジェクトです。名前はPEP 8の規約で、言語の予約語ではありません。- 主な用途は
from_dict()のような代替コンストラクタです。return cls(...)と書けば、サブクラスから呼んだときにサブクラスのインスタンスが返ります。 - 選び方はメソッド本体が何に触るかで決まります。selfに触るならインスタンスメソッド、clsに触るならクラスメソッドで、どちらにも触らない処理はスタティックメソッドより先にモジュール関数を検討します。
- 代替コンストラクタの戻り値は
-> Self(Python 3.11以上)で注釈します。-> "Base"と書くと、サブクラスのメソッドを呼んだ行でmypyがエラーを出します。 cls.count += 1はサブクラスから呼ぶとサブクラス側に別の変数を作ります。全体で共有する値はクラス名で更新します。@classmethodと@propertyの重ね掛けは3.13で削除されました。3.12までは警告なしで動くので、移行前にコードを検索して書き換えます。
Pythonのclsとは:@classmethodの第一引数に入るクラス
defの直前に@classmethodを置くと、そのメソッドはクラスから直接呼べるようになり、第一引数には呼び出したクラスが入ります。Python公式ドキュメントの組み込み関数classmethodは「クラスメソッドは、インスタンスメソッドが暗黙の第一引数としてインスタンスをとるように、第一引数としてクラスをとります。」と説明しています。
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@classmethod
def from_fahrenheit(cls, fahrenheit):
return cls(round((fahrenheit - 32) / 1.8, 1))
t = Temperature.from_fahrenheit(212)
print(t.celsius)
# 100.0
clsとselfの違い
違いは第一引数に何が入るかだけです。selfにはt.method()のドットの左にあるインスタンスが、clsにはTemperature.from_fahrenheit()のドットの左にあるクラスが入ります。そのためclsからはクラス変数には届きますが、self.celsiusのようなインスタンス変数には届きません。
PEP 8は「Always use cls for the first argument to class methods.」(クラスメソッドの第一引数には常にclsを使う)と定め、インスタンスメソッドのselfにも同じ規約を置いています。
@classmethodの@はデコレータ構文
先頭の@はクラスメソッド専用の記号ではなく、デコレータ構文です。直後の関数をclassmethod()に通した結果を同じ名前に置き直す、という意味になり、次の2つは同じ動きをします。
class A:
@classmethod
def f(cls):
return cls.__name__
class B:
def f(cls):
return cls.__name__
f = classmethod(f)
print(A.f(), B.f())
# A B
@staticmethodや@property、フレームワークのデコレータも同じ仕組みです。複数重ねると下から順に適用されるため、後述するPydanticのように並べる順序が結果を変えることがあります。
cls()とreturn clsの意味
clsはクラスそのものなので、cls(...)はTemperature(...)と書くのと同じくインスタンスを作ります。クラスメソッドの中でreturn cls(...)と書くのは「呼び出したクラスのインスタンスを返す」という意味です。クラス名を直接書かずにclsを使う理由は、継承したときに効いてきます(後述の「継承したときにclsが指すクラス」)。一方、return clsと括弧なしで書くと、インスタンスではなくクラスオブジェクトそのものが返ります。チェーンの途中でクラスを返したい特殊な場合以外は、括弧の付け忘れを疑ってください。
インスタンスメソッド・クラスメソッド・スタティックメソッドの違い
Pythonのクラスに書くメソッドは3種類で、違いは第一引数に何が自動で渡るかです。
class Order:
tax_rate = 0.1
def __init__(self, amount):
self.amount = amount
def total(self):
# インスタンスメソッド: self.amount を使う
return self.amount * (1 + self.tax_rate)
@classmethod
def set_tax_rate(cls, rate):
# クラスメソッド: クラス変数を書き換える
cls.tax_rate = rate
@staticmethod
def is_valid_amount(value):
# スタティックメソッド: self も cls も受け取らない
return isinstance(value, int) and value > 0
print(Order(1000).total())
# 1100.0
Order.set_tax_rate(0.08)
print(Order(1000).total())
# 1080.0
print(Order.is_valid_amount(-5))
# False
第一引数と呼び出し方の比較表
| 種類 | デコレータ | 第一引数 | 触れる対象 | インスタンスなしで呼ぶ |
|---|---|---|---|---|
| インスタンスメソッド | なし | self(インスタンス) | インスタンス変数・クラス変数 | 不可 |
| クラスメソッド | @classmethod | cls(クラス) | クラス変数・cls()による生成 | 可 |
| スタティックメソッド | @staticmethod | 自動では渡らない | 引数で渡した値 | 可 |
公式ドキュメントは「クラスメソッドは C++ や Java の静的メソッドとは異なります。静的メソッドは、この節の staticmethod() を参照してください。」と注意しています。他言語の静的メソッドに当たるのは@staticmethodのほうで、@classmethodはクラスを受け取る分だけできることが多い、と覚えると混同しません。
どれを選ぶかの判断順序
メソッドの中で何を使うかを、上から順に確かめれば決まります。
self.amountのようにインスタンスの値を使う → インスタンスメソッド- インスタンスは使わないが、クラス変数か
cls(...)による生成を使う → クラスメソッド - どちらも使わず引数だけで完結する → スタティックメソッド、またはモジュール関数
3番目は、まずモジュール関数で足りないかを考えます。クラスに置く価値があるのは、Order.is_valid_amountのように「どのクラスの道具か」が名前から伝わる場合と、サブクラスで差し替えたい場合です。迷ったらインスタンスメソッドを既定にしてください。後からインスタンスの値が必要になったとき、クラスメソッドからの書き換えは呼び出し側まで波及します。
代替コンストラクタをfrom_で始まるクラスメソッドで書く
クラスメソッドの用途の中心は代替コンストラクタです。公式ドキュメントはstaticmethodの項で「別のコンストラクタをクラスに用意する際に便利な @classmethod も参照してください」と案内しています。__init__は1クラスに1つしか書けませんが、辞書から、CSVの1行から、APIの応答から、と生成経路は増えていきます。この分岐を__init__の引数と型判定で吸収すると引数が膨らむので、経路ごとにクラスメソッドを用意します。
辞書やCSVの行からインスタンスを作る
class User:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def from_dict(cls, data):
return cls(data["name"], data["age"])
@classmethod
def from_csv_row(cls, row):
name, age = row.split(",")
return cls(name.strip(), int(age))
u1 = User.from_dict({"name": "taro", "age": 30})
u2 = User.from_csv_row("hanako, 25")
print(u1.name, u2.age)
# taro 25
標準ライブラリも同じ発想の命名です。dict.fromkeys()、int.from_bytes()、datetime.fromisoformat()はどれも「何から作るか」を名前に含めています。自作の代替コンストラクタもfrom_で始めておけば、読み手は中身を見る前に用途を判断できます。生成するクラス自体を入力によって切り替えたい場合はファクトリーメソッドの領域で、PythonのFactory Method(ファクトリメソッド)実装|ファクトリ関数・classmethodとの使い分けで書き分けを扱っています。
戻り値の型ヒントはtyping.Selfで書く
代替コンストラクタの戻り値は、Python 3.11で追加されたtyping.Selfで注釈します。公式ドキュメントのtyping.Selfは、selfを返すメソッドに続く「他によくある用途」として「代替コンストラクタとして使われ、cls 引数のインスタンスを返す classmethod」を挙げています。クラス名を文字列で書く-> "Base"との違いは、型チェッカーに通すと分かります。
from typing import Self
class Base:
def __init__(self, value: int) -> None:
self.value = value
@classmethod
def from_str(cls, text: str) -> Self:
return cls(int(text))
@classmethod
def from_str_base(cls, text: str) -> "Base":
return cls(int(text))
class Child(Base):
def double(self) -> int:
return self.value * 2
c = Child.from_str("21")
print(type(c).__name__, c.double())
# Child 42
d = Child.from_str_base("21")
print(type(d).__name__, d.double())
# Child 42
実行するとどちらもChild 42を出します。ところが末尾にreveal_type(c)とreveal_type(d)を足してmypy 2.4.1で検査すると、reveal_type(c)はChild、reveal_type(d)はBaseと推論され、最後のd.double()の行にerror: "Base" has no attribute "double"が出ました。実行時は正しく動くのに、注釈のせいで型検査だけが落ちる状態です。cls(...)で生成しているならSelf、メソッド内でクラス名を直接書いて常に親を返すなら"Base"、と実装に合わせてください。公式ドキュメントも、サブクラスのインスタンスを返す保証がないメソッドにSelfを使うべきではないと書いています。
Python 3.10でfrom typing import Selfを実行するとImportError: cannot import name 'Self' from 'typing'で止まります。3.10以前も対象にするなら、外部パッケージtyping_extensionsから読み込みます。
継承したときにclsが指すクラス
公式ドキュメントは「クラスメソッドが派生クラスから呼び出される場合は、その派生クラスオブジェクトが暗黙の第一引数として渡されます」と定めています。親クラスで定義したメソッドでも、clsに入るのは呼び出したサブクラスです。
return cls()とreturn Base()の差
class Base:
@classmethod
def create(cls):
return cls()
@staticmethod
def create_base():
return Base()
class Sub(Base):
pass
print(type(Sub.create()).__name__)
# Sub
print(type(Sub.create_base()).__name__)
# Base
Sub.create()はBaseに書いたメソッドを実行しながら、Subのインスタンスを返します。クラス名を直接書いたcreate_base()は、サブクラスから呼んでも常にBaseを返します。スタティックメソッドは自分がどのクラスとして呼ばれたかを知る手段を持たないので、継承先で結果を変えたい生成処理には使えません。「サブクラスごとに結果が変わるべきか」が、classmethodとstaticmethodを分ける実務上の判断軸です。
super()で親のクラスメソッドを再利用する
サブクラスで代替コンストラクタを上書きするときは、super().from_dict(data)で親の処理を呼べます。このとき親のメソッドに渡るclsはサブクラスのままなので、親のcls(...)はサブクラスのインスタンスを作ります。
class Shape:
def __init__(self, name):
self.name = name
@classmethod
def from_dict(cls, data):
return cls(data["name"])
class Circle(Shape):
def __init__(self, name, radius=1.0):
super().__init__(name)
self.radius = radius
@classmethod
def from_dict(cls, data):
obj = super().from_dict(data) # cls=Circle のまま親の処理を使う
obj.radius = data.get("radius", 1.0)
return obj
c = Circle.from_dict({"name": "c1", "radius": 2.5})
print(type(c).__name__, c.name, c.radius)
# Circle c1 2.5
__init_subclass__と抽象クラスメソッド
サブクラスが定義された時点で呼ばれる__init_subclass__は、デコレータを書かなくてもクラスメソッドとして扱われます。データモデルの説明には「もし、このメソッドがインスタンスメソッドとして定義されると、暗黙的にクラスメソッドに変換されます」とあり、メタクラスなしでサブクラスの登録処理を書けます。
class Plugin:
registry = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Plugin.registry.append(cls.__name__)
class CsvPlugin(Plugin):
pass
class JsonPlugin(Plugin):
pass
print(Plugin.registry)
# ['CsvPlugin', 'JsonPlugin']
サブクラスに実装を強制するクラスメソッドは、@classmethodを上、@abstractmethodを下に重ねて書きます。abcモジュールの@abstractclassmethodはPython 3.3で非推奨になっており、新しく書くコードで使う理由はありません。
クラス変数をクラスメソッドで更新するときの落とし穴
クラス変数の更新口をクラスメソッドにまとめるのはよくある設計ですが、継承が絡むとclsの性質が裏目に出ます。
cls.count += 1はサブクラス側に別の変数を作る
class Session:
active = 0
@classmethod
def open(cls):
cls.active += 1
return cls()
class AdminSession(Session):
pass
Session.open()
AdminSession.open()
AdminSession.open()
print(Session.active, AdminSession.active, "active" in AdminSession.__dict__)
# 1 3 True
3回開いたのにSession.activeは1のままです。AdminSession.open()ではclsがAdminSessionなので、cls.active += 1は「親の値1を読み、AdminSession自身に2を書く」動作になり、以後AdminSessionは自分専用のactiveを持ちます。Python 3.10・3.15.0のどちらでも同じ結果でした。全サブクラスで共有するカウンタならSession.active += 1とクラス名で書き、インスタンス生成だけをcls()に任せます。逆に、サブクラスごとに別々に数えたいならcls.activeの動作は意図どおりです。どちらを望むのかを決めてから書いてください。
インスタンス経由の代入と可変オブジェクトのクラス変数
インスタンスからs.active = 99と代入すると、クラス変数は変わらず、そのインスタンスにだけ同名のインスタンス変数が作られます。更新はクラスメソッドかクラス名経由に限定します。また、クラス変数に置いたリストや辞書は全インスタンスとサブクラスで共有されるので、Plugin.registryのように意図して共有する場合以外は、__init__の中でself.items = []と初期化してください。
@classmethodを付け忘れたときのTypeError
デコレータの付け忘れは、missingの後ろに出る引数名がどれかで見分けられます。
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def from_fahrenheit(cls, fahrenheit): # @classmethod を付け忘れた
return cls(round((fahrenheit - 32) / 1.8, 1))
def describe(self):
return f"{self.celsius}℃"
@classmethod
def describe_all(cls):
return cls.describe() # インスタンスメソッドを cls から呼んだ
for f in (lambda: Temperature.from_fahrenheit(212), lambda: Temperature.describe_all()):
try:
f()
except TypeError as e:
print("TypeError:", e)
# TypeError: Temperature.from_fahrenheit() missing 1 required positional argument: 'fahrenheit'
# TypeError: Temperature.describe() missing 1 required positional argument: 'self'
1つめは、デコレータがないので212がclsの位置に入り、fahrenheitが足りなくなっています。「渡した覚えのある引数が足りないと言われる」ときは、デコレータの付け忘れを疑います。2つめは、cls.describe()がクラスからインスタンスメソッドを呼ぶのと同じなのでselfに入るものがありません。インスタンスの値が要る処理は、クラスメソッドからは呼べないということです。メッセージはPython 3.10と3.15.0で同じ文面でした。
classmethodの仕組み:デスクリプタの__get__がclsを束縛する
clsが自動で入る仕組みは、デスクリプタ(属性アクセスに割り込むオブジェクト)で説明できます。公式のデスクリプタ HowTo ガイドには、classmethodと同じ動きをする純粋Pythonの実装が載っています。これを動かすと、組み込みと同じ結果になります。
import functools
from types import MethodType
class ClassMethod:
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
functools.update_wrapper(self, f)
def __get__(self, obj, cls=None):
if cls is None:
cls = type(obj)
return MethodType(self.f, cls)
class A:
@ClassMethod
def who(cls):
return cls.__name__
class B(A):
pass
print(A.who(), B.who(), B().who())
# A B B
print(A.__dict__["who"].__get__(None, B))
# <bound method A.who of <class '__main__.B'>>
B.whoと書いた瞬間に__get__(None, B)が呼ばれ、関数の第一引数をBで埋めた束縛メソッドが返ります。インスタンスB()から呼んだときも、属性検索の仕組みが__get__(b, type(b))の形でクラスを渡すので、clsにはBが入ります(if cls is Noneの分岐は__get__(obj)を直接呼ばれたときの保険です)。「インスタンスから呼んでもclsにはクラスが入る」「サブクラスから呼ぶとサブクラスが入る」という前の章の挙動は、この数行と属性検索の仕組みから出てきます。デスクリプタを含むPythonのメタプログラミング全般は扱わず、ここではclsの正体を確かめるところまでにとどめます。
標準ライブラリとPydanticでのクラスメソッド
標準ライブラリの代替コンストラクタが実際にクラスメソッドかどうかは、inspect.getattr_static()でデスクリプタを取り出すと確認できます。
import datetime
import inspect
import pathlib
for owner, name in [(dict, "fromkeys"), (int, "from_bytes"),
(datetime.datetime, "fromisoformat"), (pathlib.Path, "cwd")]:
print(f"{owner.__name__}.{name}:", type(inspect.getattr_static(owner, name)).__name__)
# dict.fromkeys: classmethod_descriptor
# int.from_bytes: classmethod_descriptor
# datetime.fromisoformat: classmethod_descriptor
# Path.cwd: classmethod
C実装の型はclassmethod_descriptor、Pythonで書かれたpathlibはclassmethodと表示が違うだけで、どちらもクラスメソッドです。Path.cwd()の使い方はPath(__file__).resolve().parentとは|Python pathlibのresolve()・absolute()の違いとBASE_DIRの書き方【3.15対応】で扱っています。
フレームワークでは、Pydanticとは?読み方・BaseModelの使い方とV1との違い【2.13対応】で扱うPydantic v2のfield_validator(およびbefore・wrapのmodel_validator)がクラスメソッドで書かれます。Pydanticのバリデータのドキュメントの例はすべて@field_validatorを上、@classmethodを下に重ねています。値の検証はインスタンスの生成途中で走るので、まだ完成していないインスタンスのselfではなくクラスを受け取ります。
from pydantic import BaseModel, field_validator
class Good(BaseModel):
name: str
@field_validator("name", mode="after")
@classmethod
def strip_name(cls, value: str) -> str:
return value.strip()
class Reversed(BaseModel):
name: str
@classmethod
@field_validator("name", mode="after")
def strip_name(cls, value: str) -> str:
return value.strip()
print(repr(Good(name=" taro ").name), repr(Reversed(name=" taro ").name))
# 'taro' ' taro '
pydantic 2.14.2で、順序を逆にしたReversedは例外も警告も出さずに検証を素通りさせ、前後の空白が残りました。-W alwaysで警告を全表示にしても何も出ません。Pydantic自身は順序の誤りを知らせないので、後述の重ね掛け検出スクリプトと同じ要領で@classmethodが@field_validatorより上にある関数を探すか、プロジェクトのテンプレートに正しい順序を固定しておきます。
バージョンで変わったclassmethodの仕様
組み込み関数の説明に載っている変更は、3.9のデスクリプタのラップ対応、3.10の属性継承、3.11の非推奨と3.13の削除の3つです。Python 3.14とPython 3.15の新機能|遅延インポートとUTF-8既定化、3.14から上げる手順で扱う3.15では、classmethod自体の変更はありません。
Python 3.13で削除されたpropertyとの重ね掛け
括弧なしでConfig.nameと読めるクラスプロパティを作る目的で、次の書き方が使われてきました。
import sys
class Config:
_name = "prod"
@classmethod
@property
def name(cls):
return cls._name
print(sys.version.split()[0], repr(Config.name))
Python 3.9で「クラスメソッドは property() など、他のデスクリプタをラップすることができるようになりました」と変更されたことで動くようになり、3.11で非推奨、3.13で削除されました。3.13の変更点は削除の理由を「この機能には基本的な設計上の欠陥があり、いくつもの問題を引き起こしていました」と説明しています。各版で実行した結果は次のとおりです。
| 版 | Config.nameの値 | 警告 |
|---|---|---|
| 3.9.6 | ‘prod’ | なし |
| 3.10.20 | ‘prod’ | なし |
| 3.11.15 | ‘prod’ | なし |
| 3.12.13 | ‘prod’ | なし |
| 3.13.14 | 束縛メソッド | なし |
| 3.14.6 | 束縛メソッド | なし |
| 3.15.0 | 束縛メソッド | なし |
2026年11月時点の最新は3.15.2・3.14.10ですが、上の表はそれより前のパッチ版で実行した結果です。
非推奨とされた3.11と3.12でもDeprecationWarningは出ず、正しく動きます。3.13以降は例外にもならず、Config.nameが<bound method name of <class '__main__.Config'>>を返します。f文字列やログに混ぜている箇所はそのまま通り、Config.name.upper()のように文字列として扱った行で初めてAttributeErrorになります。壊れた場所と落ちる場所が離れるので、Python 3.13の新機能と3.12との違い|3.13.17の変更点・サポート期限・GIL無効化の実測への移行前に、コード側から探しておく必要があります。書き換えは、括弧付きで呼ぶ普通のクラスメソッドに戻すのが最も単純です。
class Config:
_name = "prod"
@classmethod
def name(cls):
return cls._name
print(Config.name())
# prod
重ね掛けをコードベースから探すスクリプト
警告が出ない以上、実行時のログからは移行対象を拾えません。標準のastモジュールでデコレータの並びを調べれば、@classmethodの直下に@propertyがある関数を一覧にできます。grepと違い、間に空行やコメントがあっても、@builtins.classmethodと書かれていても拾えます。
import ast
import pathlib
import sys
def name_of(dec):
# @classmethod と @builtins.classmethod を同じ名前に直す
if isinstance(dec, ast.Name):
return dec.id
if isinstance(dec, ast.Attribute):
return dec.attr
return None
root = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else ".")
for path in root.rglob("*.py"):
try:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
except (SyntaxError, UnicodeDecodeError) as e:
print(f"skip {path}: {e}", file=sys.stderr)
continue
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
names = [name_of(d) for d in node.decorator_list]
for upper, lower in zip(names, names[1:]):
if upper == "classmethod" and lower == "property":
print(f"{path}:{node.lineno}: {node.name}")
python find_classproperty.py srcのように対象ディレクトリを渡すと、src/config.py:9: nameの形でdefの行番号を出します(3.9.6・3.10.20・3.15.0で同じ結果)。CIに組み込めば新たな混入も止められます。構文エラーのファイルはskipと出して飛ばします。@functools.cached_propertyなど他のデスクリプタとの重ね掛けも3.13で使えなくなるので、それらも探すならlower == "property"の条件を広げてください。
Python 3.10で変わったメソッド属性とstaticmethodの呼び出し
3.10から、クラスメソッドとスタティックメソッドは元の関数の__module__・__name__・__qualname__・__doc__・__annotations__を引き継ぎ、__wrapped__属性を持つようになりました。3.9以前は、クラスの__dict__から取り出したclassmethodオブジェクト自体の__doc__がclassmethod型の説明文になり、__wrapped__もありません。同じ3.10で、スタティックメソッドはクラス本体の中からf()と普通の関数のように呼べるようになりました。
よくある質問
クラスメソッドはインスタンスから呼び出せますか?
呼び出せます。公式ドキュメントも、クラスから(C.f())でもインスタンスから(C().f())でも呼べ、インスタンスはそのクラス以外無視されると明記しています。ただしt.from_fahrenheit()のように書くと、読み手はtの値を使う処理だと誤読します。Temperature.from_fahrenheit()とクラス名で呼べば、インスタンスに依存しないことが呼び出し行だけで分かります。
3.13以降で括弧なしのクラスプロパティを作るには?
メタクラスに@propertyを定義します。class ConfigMeta(type):にnameプロパティを書き、class Config(metaclass=ConfigMeta):とすると、Config.nameもサブクラスのStagingConfig.nameも括弧なしで読めました(3.10・3.15.0で確認)。ただしインスタンスからは読めず、メタクラスは他のメタクラスを使うクラスと衝突しやすいので、普通はConfig.name()で足ります。
クラスメソッドから別のクラスメソッドやスタティックメソッドを呼ぶには?
cls.helper()のようにcls経由で呼びます。クラス名でOrder.helper()と書くと、サブクラスでhelperを上書きしても親の実装が呼ばれます。メソッドの中からhelper()と名前だけで呼ぶとNameErrorになります。
クラスメソッドの呼び出しは遅いですか?
timeit.repeat(number=2_000_000, repeat=5)の最小値を1回あたりに換算すると、Python 3.15.0(Intel Core i9-9880H)でモジュール関数約35ns、インスタンスメソッド約40ns、スタティックメソッド約65ns、クラスメソッド約85nsでした(計測のたびに数ns揺れます)。3.14.6でも順位は同じで、クラスメソッドはインスタンスメソッドの約2倍です。差は数十ナノ秒なので、1秒間に数百万回呼ぶ内側のループでない限り、設計の意図で選んで構いません。
clsという名前は別の名前に変えても動きますか?
動きます。第一引数の名前は言語仕様では決まっておらず、klassやcでもクラスが渡されます。ただ、PEP 8がclsを使うよう定めており、リンターや読み手もこの名前を前提にするため、変える利点はありません。classは予約語なので第一引数の名前には使えません。
クラス設計の段階から業務システムをPythonで作りたい場合は、フルスクラッチ開発でご相談いただけます。