•11 min read

Python's Underscores: Conventions, Not Access Modifiers

Python's Underscores: Conventions, Not Access Modifiers
Python Underscores Explained

Here's the deal: Python doesn't enforce access control. There's no real private, protected, or public barrier. Instead, underscores are naming signals—with one special behavior for double underscores that trips up almost everyone.


Audio Briefing
0:00 / 0:00

The Three Levels of "Privacy"

PatternMeaningEnforced?Use CaseRuntime Behavior
Public API
name
Stable interface for users
No (by convention)
Library functions, class methods
Fully accessible
Internal Use
_name
Implementation details, helpers
No
Helper methods, internal state
Accessible, linter warns
Name Mangling
__name
Prevents subclass name collisions
Yes (name change)
Avoid subclass collisions
Renamed to _ClassName__name
Magic Methods
__name__
Protocol implementation
N/A (special)
__init__, __str__, __len__
Called by Python internals

No Underscore → Public API

This is your contract with users. Changing it is a breaking change.

class User:
    def get_name(self) -> str:      # ✅ Public: safe to use
        return self.name
    
    def calculate_age(self) -> int: # ✅ Public: part of API
        return 2024 - self.birth_year

Single Underscore `_name` → Internal Convention

A gentleman's agreement that this is for internal use. Python won't stop you, but linters and IDEs will warn you.

class User:
    def _validate_email(self) -> bool:  # 🔒 Internal: don't call directly
        return "@" in self.email
    
    def _cache_key(self) -> str:        # 🔒 Internal: implementation detail
        return f"user:{self.id}"
What single underscore actually does
  • Signals "internal use only" to other developers
  • Excluded from import * by default
  • Linters (ruff, pyflakes) will warn on external access
  • Zero runtime enforcement — you can still call it!

Double Underscore `__name` → Name Mangling

This actually changes the name at runtime to prevent accidental collisions in inheritance—not to enforce privacy.

class Base:
    def __secret(self) -> str:
        return "base secret"
    
    def call_secret(self) -> str:
        return self.__secret()  # Works! Calls Base.__secret

class Derived(Base):
    def __secret(self) -> str:     # Different name: _Derived__secret
        return "derived secret"
    
    def call_both(self) -> tuple:
        return (self.__secret(), super().call_secret())
        # Returns: ("derived secret", "base secret")
Name mangling is NOT privacy

You can still access it: obj._Base__secret() works perfectly fine. It's just harder to do by accident.


Advertisement

Visual Summary

PatternMeaningEnforced?Use Case
Public API
<code>name</code>
Public API
No (by convention)
Stable interface for users
Internal Use
<code>_name</code>
Internal use
No
Implementation details, helpers
Name Mangling
<code>__name</code>
Name mangling
Yes (name change)
Avoid subclass collisions
Magic Methods
<code>__name__</code>
Dunder/magic methods
N/A
Protocol implementation

Real-World Patterns

Use single underscore for methods subclasses should override:

class DatabaseConnection:
def connect(self) -> Connection:
    """Public API: establish connection."""
    self._validate_config()
    conn = self._create_connection()
    self._post_connect_setup(conn)
    return conn

# Override these in subclasses:
def _validate_config(self) -> None:
    """Check required config exists."""
    if not self.host:
        raise ValueError("Host required")

def _create_connection(self) -> Connection:
    """Actual connection logic — override for different DBs."""
    return pg_connect(self.host, self.user, self.password)

def _post_connect_setup(self, conn: Connection) -> None:
    """Hook for subclasses to run after connect."""
    conn.set_timezone("UTC")
Why this works

Subclasses override _create_connection for MySQL, MongoDB, etc. without touching the public connect() method. The single underscore says "override me" without enforcing it.

Double underscore in __slots__ is a special case — it's a class-level declaration, not name mangling:

class Point:
__slots__ = ('x', 'y', '_cached_hash')  # No __dict__ created!

def __init__(self, x: float, y: float):
    self.x = x
    self.y = y
    self._cached_hash = None  # Internal cache

def __hash__(self) -> int:
    if self._cached_hash is None:
        self._cached_hash = hash((self.x, self.y))
    return self._cached_hash

# Memory efficient: no __dict__ per instance
p = Point(1, 2)
# p.z = 3  # AttributeError — can't add arbitrary attributes
Performance note

slots saves ~40-50% memory per instance by avoiding dict. Use for classes with many instances (dataclasses, game entities, data rows).


Comparison: Python vs Other Languages

Java / C#

Enforced access modifiers

  • private — truly inaccessible
  • protected — subclass only
  • public — anyone
  • Compiler enforces at build time

Python

Convention-based

  • name — public API
  • _name — internal (warning only)
  • __name — name mangled
  • Runtime: zero enforcement

Rust

Module-based privacy

  • pub — public
  • Default — module private
  • pub(crate) — crate public
  • Compiler enforces at build time

Advertisement

Decision Flowchart


Best Practices Checklist

✅ Do❌ Don't
Naming
Use `_name` for internal helpers
Use `__name` for "privacy"
Documentation
Document public API with docstrings
Rely on underscores for security
Testing
Test through public methods
Test private methods directly
Optimization
Use `__slots__` for memory optimization
Add `__slots__` without profiling first
Respect
Respect `_name` in other's code
Access `obj._internal` in production

TL;DR

Remember

Python trusts you. Underscores are communication tools, not walls.

  • name → "Use this freely"
  • _name → "Internal detail, may change"
  • __name → "Won't clash with subclass names"
  • name → "Magic method for Python protocols"
Python OOP Best Practices

You Might Also Like

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement