This rule raises an issue when private attributes in data validation or modeling frameworks use a double leading underscore prefix (like __attr).

In Python, this specifically applies to Pydantic models.

Why is this an issue?

In data validation frameworks, private attributes are typically defined using a specific naming convention (e.g., prefixing the attribute name with a single underscore). These attributes are not validated during object instantiation and are excluded from the schema or type definition.

When an attribute is declared with a double leading underscore (e.g., __attr), Python applies name mangling at the class body level, renaming the attribute to _ClassName__attr before the framework even sees it. The class will define and instantiate without any error, but this creates a silent behavioral trap:

This behavior causes subtle bugs that are difficult to detect, because the class appears to be defined correctly and instantiation succeeds without any warning.

In Pydantic, private attributes must be declared with a single leading underscore (e.g., _private_attr). When an attribute is declared with a double leading underscore (e.g., __private_attr), Python’s name mangling silently renames it to _ClassName__private_attr before Pydantic processes the class. This causes two concrete problems:

What is the potential impact?

Using a double leading underscore for private attributes in data modeling frameworks leads to:

How to fix it

Replace the double leading underscores with a single underscore prefix. Pydantic recognizes single-underscore attributes as private and initializes them correctly.

Code examples

Noncompliant code example

from pydantic import BaseModel, PrivateAttr

class Model(BaseModel):
    __counter: int = PrivateAttr(default=0)  # Noncompliant: name mangling prevents expected access

m = Model()
print(m.__counter)  # Raises AttributeError: 'Model' object has no attribute '__counter'

Compliant solution

from pydantic import BaseModel, PrivateAttr

class Model(BaseModel):
    _counter: int = PrivateAttr(default=0)  # Compliant: single underscore prefix

m = Model()
print(m._counter)  # Works correctly: outputs 0

Resources

Documentation