The Silent Registration Killer: When Auto-Formatters and Linters Collide

Published: (January 11, 2026 at 09:35 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

The Unexpected Registration Bug

A feature was shipped, manual tests looked “okay‑ish,” and the code went to staging.
Soon after, users hit the registration endpoint and received a generic 500 Internal Server Error.
The server logs showed:

TypeError: CustomUser() got unexpected keyword arguments: 'password2'

The code appeared correct at first glance, but a series of “helpful” tools had introduced a subtle bug.

How a Formatting Slip Caused a TypeError

In the project a ModelSerializer was used for registration and a password2 field was added for “Confirm Password” validation. During a cleanup or auto‑formatting run, the line that removed password2 from the incoming data was accidentally shifted or commented out.

def create(self, validated_data):
    # Extract the main password
    password = validated_data.pop("password")

    # Oops – `password2` was left in the dictionary!
    # validated_data might look like:
    # {"email": "user@example.com", "password2": "secret123"}

    user = User.objects.create_user(**validated_data)  # <-- crash point

    user.set_password(password)
    user.save()
    return user

Because validated_data was passed directly to create_user, Django tried to pass password2 as a keyword argument to the User model, which has no such field, resulting in the TypeError. The UI only displayed a vague “Server Error,” leaving users stranded.

The Linter Conflict

To catch such “dirty” dictionaries, the team introduced Ruff via pre‑commit hooks. When the developer tried to fix the error by popping the variable, Ruff complained:

password2 = validated_data.pop("password2")
F841 Local variable 'password2' is assigned to but never used

The instinctive reaction was to silence the rule:

- id: ruff
  args: ["--fix", "--unfixable=F841"]

However, --unfixable only prevents Ruff from automatically fixing the issue; it does not ignore the error, so the pre‑commit hook still failed.

Proper Ways to Pop Unused Data

“Dirty” Fix (triggers F841)

password2 = validated_data.pop("password2")

Clean Fix (no variable assignment → no linter error)

validated_data.pop("password2", None)

Pythonic Fix (underscore signals intentional unused variable)

_password2 = validated_data.pop("password2")

The underscore prefix tells both readers and linters that the variable is intentionally ignored.

Instead of fighting the linter, configure it to work with you. Below is a battle‑tested ruff.toml that balances strictness and Django‑specific realities.

# ruff.toml
target-version = "py312"
line-length = 88

[lint]
# Enable a wide range of rules
select = [
    "F",   # Pyflakes (unused variables, etc.)
    "E",   # Error (standard style)
    "W",   # Warning
    "I",   # Isort (import ordering)
    "DJ",  # Django‑specific rules
    "B",   # Bugbear (common design flaws)
    "SIM", # Simplicity (cleaner code)
]

# Ignore rules that are too noisy for the team
ignore = [
    "DJ001", # Direct User imports sometimes necessary in managers
]

[lint.per-file-ignores]
# Ignore unused imports in package initializers
"__init__.py" = ["F401"]
# Ignore line‑length and unused‑import warnings in auto‑generated migrations
"**/migrations/*" = ["E501", "F401"]

[format]
quote-style = "double"
indent-style = "space"

Additional Django Tip

Always use get_user_model() (or a string reference like 'myapp.CustomUser') for model references instead of importing the concrete User class directly. This avoids circular imports and hard‑coding.

Takeaways

  • Linting is a guardrail, not a hurdle. When Ruff flags an unused variable, it’s often warning about “garbage” data that could break downstream logic.
  • Pop unused keys without assigning (validated_data.pop("password2", None)) satisfies the linter and prevents TypeErrors.
  • Configure your linter rather than silencing it; a well‑tuned ruff.toml keeps the codebase clean while respecting Django’s quirks.
  • Explicit is better than implicit. Clear data handling makes functions pure, logs cleaner, and future maintenance easier.

By applying the clean pop pattern and a sensible Ruff configuration, the team eliminated the registration crash, satisfied the linter, and restored a smooth user experience.

Back to Blog

Related posts

Read more »