Long Parameter List: group related arguments into objects

Nine positional arguments are easy to mix up. Learn to group related values into objects and make options keyword-only, with tested before and after code.

Many separate arguments grouped into two objects

A Long Parameter List is a function that takes four, five, nine arguments. The call is hard to read (create_user("a", "b", 24, "c", ...)), easy to get wrong (two booleans swapped), and hard to extend. The code and output below were run with Python 3 before publishing.

How to recognise it

  • You count arguments on your fingers.
  • Calls have several literals in a row, and you cannot tell what each one means without opening the function.
  • Two parameters of the same type sit next to each other (two booleans, two strings), so swapping them still runs.
  • Adding one more option means changing every caller.
  • Several parameters always travel together (street, city, zip, country).

The fixes

  1. Introduce a Parameter Object. Group values that belong together (an Address, a Contact) into one object.
  2. Preserve Whole Object. If the function takes three fields of an object, pass the object.
  3. Make options keyword-only with defaults. A * in the signature forces callers to write newsletter=True, which documents itself.
from dataclasses import dataclass, field

# ---- BEFORE: nine positional parameters, easy to mix up ----
def create_user_before(name, email, age, street, city, zip_code, country, newsletter, active):
    return {"name": name, "email": email, "age": age, "address": f"{street}, {city} {zip_code}, {country}", "newsletter": newsletter, "active": active}

# ---- AFTER: related values grouped, options keyword-only with defaults ----
@dataclass(frozen=True)
class Address:
    street: str
    city: str
    zip_code: str
    country: str
    def __str__(self): return f"{self.street}, {self.city} {self.zip_code}, {self.country}"

@dataclass(frozen=True)
class Contact:
    name: str
    email: str
    age: int

def create_user(contact: Contact, address: Address, *, newsletter=False, active=True):
    return {"name": contact.name, "email": contact.email, "age": contact.age, "address": str(address), "newsletter": newsletter, "active": active}

The new create_user takes two objects and two named options. A call reads like a sentence, the address validates in one place, and new options can be added with a default without breaking old callers.

Prove the result is the same, and see the old trap

old = create_user_before("Amina", "a@example.com", 24, "12 Road", "Dhaka", "1207", "BD", True, True)
new = create_user(Contact("Amina", "a@example.com", 24), Address("12 Road", "Dhaka", "1207", "BD"), newsletter=True)
print(new)
assert old == new
# The old call, with the last two flags swapped by mistake, still runs and is silently wrong:
print(create_user_before("Amina", "a@example.com", 24, "12 Road", "Dhaka", "1207", "BD", False, True)["newsletter"])
try: create_user(Contact("A", "a@x.com", 1), Address("s", "c", "z", "k"), True)
except TypeError as e: print("error:", e)
{'name': 'Amina', 'email': 'a@example.com', 'age': 24, 'address': '12 Road, Dhaka 1207, BD', 'newsletter': True, 'active': True}
False
error: create_user() takes 2 positional arguments but 3 were given

The new call gives exactly the same dictionary as the old one. The old function with newsletter and active swapped by mistake still runs and gives a wrong answer (False for the newsletter flag) because both are booleans. The new version refuses a positional True, since options must be named, so the mistake becomes an immediate TypeError.

How to refactor safely

  1. Write a test for the current function.
  2. Create the parameter objects and a new function that takes them, then make the old function call the new one.
  3. Move callers over one at a time and remove the old function.
  4. Use frozen=True dataclasses so the objects cannot change while in use.
  5. Put the * before options so they must be named.

When to leave it alone

  • Two or three clear parameters are fine.
  • Do not bundle unrelated values into a bag called Params or Options just to cut the count. A group needs a real meaning.
  • Public library functions with many options may use keyword-only arguments instead of one object.

Common mistakes

  • A "god" parameter object with every possible field.
  • Using a dictionary instead of a typed object, so typos in keys fail late.
  • Mutable parameter objects shared between calls.
  • Keeping positional booleans, which hide meaning in the call.

Main idea: group values that belong together into an object, pass whole objects instead of their pieces, and make options keyword-only. Calls become readable and mix-ups turn into errors.


Code smells and refactoring series, Bloaters: 4 of 5. Previous: Primitive Obsession: replace bare strings and floats with value objects.

Keep reading