Skip to content
Sarj Code Standards

prefer-match-type-dispatch

Prefer match for structural runtime type dispatch.

Why

A repeated mutually exclusive isinstance dispatch repeats its subject and hides the closed list of runtime shapes. Class patterns make the alternatives explicit without repeating the dispatch subject.

Fix

Replace the branches with match subject and one class-pattern arm per distinct type; combine types that share behavior with an OR-pattern.

Examples

Before — flagged A terminating prefix and conditional if/elif tail dispatch on the same AST node
app/bindings.py
import ast
def has_binding(node, name):
if isinstance(node, ast.ListComp):
return True
if isinstance(node, ast.FunctionDef):
if name in bindings(node):
return True
elif isinstance(node, ast.ClassDef):
if name in bindings(node):
return True
return False
After — preferred Class patterns preserve the nested checks within their case bodies
app/bindings.py
import ast
def has_binding(node, name):
match node:
case ast.ListComp():
return True
case ast.FunctionDef():
if name in bindings(node):
return True
case ast.ClassDef():
if name in bindings(node):
return True
return False
Before — flagged Repeated branches dispatch on runtime type
app/parser.py
def parse(value: object):
if isinstance(value, str):
return parse_text(value)
elif isinstance(value, bytes):
return parse_bytes(value)
elif isinstance(value, dict):
return parse_mapping(value)
raise TypeError(type(value))
After — preferred Class patterns make the type dispatch explicit
app/parser.py
def parse(value: object):
match value:
case str():
return parse_text(value)
case bytes():
return parse_bytes(value)
case dict():
return parse_mapping(value)
case _:
raise TypeError(type(value))
Before — flagged Repeated boolean checks share one type subject
app/types.py
def is_text(value):
if isinstance(value, str) or isinstance(value, bytes):
return True
return False
After — preferred One OR pattern declares shared handling
app/types.py
def is_text(value):
match value:
case str() | bytes():
return True
return False
Before — flagged One model-specific branch still dispatches on runtime type
app/authorization.py
from pydantic import BaseModel
class Session(BaseModel):
allowed: bool
def authorize(subject):
if isinstance(subject, Session):
if not subject.allowed:
raise PermissionError
After — preferred A class pattern keeps the nested permission check in its body
app/authorization.py
from pydantic import BaseModel
class Session(BaseModel):
allowed: bool
def authorize(subject):
match subject:
case Session():
if not subject.allowed:
raise PermissionError
case _:
return
Before — flagged Two isinstance returns project one AST identifier shape
app/ast_names.py
import ast
def dotted_tail(node: ast.expr) -> str | None:
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Attribute):
return node.attr
return None
After — preferred Class patterns expose the two AST identifier shapes
app/ast_names.py
import ast
def dotted_tail(node: ast.expr) -> str | None:
match node:
case ast.Name():
return node.id
case ast.Attribute():
return node.attr
case _:
return None
Before — flagged Two branches already repeat the same type dispatch
app/types.py
def label(value):
if isinstance(value, str):
return "text"
elif isinstance(value, bytes):
return "binary"
return "other"
After — preferred Class patterns name both alternatives
app/types.py
def label(value):
match value:
case str():
return "text"
case bytes():
return "binary"
return "other"