What does a single leading underscore on a Python module or class attribute name mean?
answer
- Python has no private keyword
- The name itself carries the promise
- Convention, not enforcement
- It changes what star import binds
- Two underscores do something different
basics
~20 sA single leading underscore marks a name as internal: not part of the public API and free to change without notice. It is a convention, not enforcement — outside code can still import and call it.
solid answer
~40 sPython has no `private` keyword, so a single leading underscore (`_compress`, `self._cache`) is the agreed signal that a name is an implementation detail: callers outside the package should not rely on it, and the author may change or delete it in any release. Nothing stops access — `import archive; archive._compress()` works fine. The convention has two real teeth. First, `from archive import *` skips underscore-prefixed names when the module defines no `__all__`. Second, tooling honours it: linters flag imports of another package's underscore names, documentation generators skip them, and type checkers treat them as private. A *double* leading underscore inside a class body is a different mechanism entirely — it triggers name mangling to `_ClassName__name`, which exists to avoid attribute collisions in subclasses, not to enforce privacy.
code
python · 10 linesimport sys, types
archive = types.ModuleType("archive") # stands in for archive.py
exec("def export(chat_id): ...\ndef _compress(blob): ...", archive.__dict__)
sys.modules["archive"] = archive
names = {}
exec("from archive import *", names)
print([n for n in names if not n.startswith("__")]) # ['export']
print(archive._compress.__name__) # '_compress' — still reachablego deeper
Be ready to state the convention in one sentence and admit plainly that Python does not enforce it. Know that _name means 'internal, may change' and that you should not import such a name from someone else's package.
Explain the mechanics: what star import binds with and without __all__, that attribute access is unaffected, and that two leading underscores in a class body mangle to _ClassName__name for collision avoidance rather than privacy.
Show how you use the convention to keep a shipped library's surface small — implementation modules behind underscore names, a deliberate promotion to public — and how you handle a downstream team that has started importing your internals.
Own the tradeoff between a permissive convention and hard enforcement. Argue why Python's social boundary is workable at scale, what CI and review must add to make it hold, and when an internal name has been used widely enough that you adopt it rather than break it.
## Python has no access modifier, so the name carries the contract Languages like Java or Kotlin let the compiler refuse a call to a private member. Python deliberately does not. Every attribute of every module, class and instance is reachable by any caller who can name it. What Python offers instead is a *naming convention* that says, in the code itself, which names are a promise and which are an implementation detail: * `name` — public. Part of the API. Changing or removing it is a breaking change you must version and announce. * `_name` — internal. "This exists for my own use; do not import it; it may vanish in the next patch release." * `__name` (two leading, at most one trailing underscore) **inside a class body** — name mangling, described below. * `__name__` — reserved for the language and its protocols. Never invent your own. The single-underscore rule applies at every level: a module-level function `_compress`, a module-level constant `_DEFAULT_CHUNK`, an instance attribute `self._cache`, a method `self._flush()`, or a whole module named `_internals.py` inside a package. ## What actually changes when you add the underscore Nothing about access. `import archive` followed by `archive._compress(blob)` runs exactly as it would without the underscore, and `getattr` sees no difference. Three things do change: **Star imports.** `from archive import *` binds every module-level name that does not start with an underscore — *when the module defines no `__all__`*. If the module does define `__all__`, that list wins completely: names not in it are skipped even if public-looking, and an underscore name listed in `__all__` is exported. So the underscore is the default filter, and `__all__` is the explicit one. **Tooling.** Linters warn when code imports a `_name` from a package it does not own. Documentation generators omit underscore members by default. Type checkers treat underscore attributes as private to the defining class or module and will complain when foreign code touches them. None of this stops a determined caller, but it makes the violation visible in review and in CI. **Human expectation.** This is the important one. Once a name is underscored, a reviewer, a downstream maintainer and a future you all read it as "no compatibility promise here". That is what lets a library keep shipping: the public names are few and stable, everything else is free to move. ## Double underscore is not "more private" Inside a class body, an identifier with two leading underscores and no more than one trailing underscore is rewritten by the compiler to `_ClassName__identifier`. `self.__key` inside `class Store` is stored as `_Store__key`, so a subclass that also uses `self.__key` gets its own `_Sub__key` and the two never collide. That is the entire purpose: collision avoidance in inheritance hierarchies, particularly for mixins and base classes designed to be subclassed by people you will never meet. It is *not* an access control mechanism, and treating it as one is the classic beginner error. `store._Store__key` retrieves the value, and `vars(store)` shows the mangled name plainly. It also has real costs: mangled attributes are awkward to inspect in a debugger, awkward to patch in a test, and confusing in subclass code. For marking a package's internal API, the single underscore is the correct tool; double underscore is reserved for the narrow collision problem. ## How this scales up to a package's public surface At the package level the convention becomes the versioning contract. A library typically puts implementation modules behind underscore names (`archive/_core.py`, `archive/_compression.py`), imports the handful of names it wants to promise into the package's `__init__.py`, and lists exactly those in `__all__`. Callers are then meant to write `from archive import export`, never `from archive._core import export`. When someone does reach into `_core`, the library author is entitled to break them — and, in practice, will, because the whole point of the underscore was to keep that module free to change. The corollary is worth stating explicitly at any level: **every non-underscored, exported name is a promise you have to keep or deprecate.** Public-by-default is how a small library grows an accidental API surface it can never shrink, so the discipline is to underscore first and promote a name to public deliberately, when you are willing to version it.
- If the underscore stops nobody, why bother with it at all?Because it is the only place the contract can live. It tells reviewers, downstream maintainers and tools which names you are willing to keep stable, so you can refactor everything else freely. Enforcement in Python is social and tooling-based rather than compiler-based, and that is sufficient: a caller who imports `pkg._core` has visibly opted out of your compatibility promise and cannot complain when it breaks.
- When would you use two leading underscores inside a class instead of one?Only when you genuinely need to avoid an attribute-name collision in a class designed for subclassing — a mixin or a framework base class where an unknown subclass might reuse the same attribute name. Name mangling gives each class its own slot. For simply marking an attribute internal, one underscore is correct; mangling makes debugging, testing and subclassing harder for no privacy gain.
- What does a trailing single underscore, as in `class_` or `id_`, signal?It is the convention for avoiding a clash with a built-in name or a keyword. `class` is a keyword and cannot be a parameter name, so APIs use `class_`; `id` and `type` are builtins you would otherwise shadow, so a local is written `id_` or `type_`. It says nothing about visibility — a trailing-underscore name can be fully public.
saying these in an interview costs you the question
- Claims a leading underscore prevents access from outside
- Says double underscore makes an attribute truly private
- Thinks name mangling is a security or encapsulation feature
- Believes star import is blocked by the underscore even with __all__ set
- Treats every helper as public because Python cannot hide it anyway