skip to content

In some languages one instance can read another instance's private state as long as both are the same class; in others privacy is per-object and even a sibling instance cannot reach it. Name concrete languages on each side, say exactly what rule each one enforces, and explain what the choice changes about how you design a type's operations.

level: seniorimportance: should knowfreq 38%

answer

  1. whose privacy: class or object
  2. Java/C#/C++/JS # = class-level
  3. Smalltalk = object-level, message sends only
  4. Ruby private = no receiver but literal self; protected = siblings
  5. Python __bal = mangling, collision avoidance not access

basics

~20 s

Java, C#, C++ and JavaScript #fields draw the boundary at the class: any instance may read a sibling's private state. Smalltalk draws it at the object. Ruby's private allows no receiver except a literal self, so sibling access needs protected.

solid answer

~50 s

The axis is *whose* privacy it is — the class's or the instance's. - **Class-level: Java, C#, C++.** Inside `Account`, `other.balance` compiles for any `Account`. That is what lets a type own binary operations — comparison, merge, diff, copy constructor — without publishing accessors. - **Class-level with a runtime brand: JavaScript.** `other.#bal` is legal inside the class body, but if `other` does not carry that private name the read throws a TypeError; `#bal in other` is the sanctioned brand check. - **Object-level: Smalltalk.** Instance variables are nameable only from the receiver's own methods, so every pairwise operation goes through message sends. - **Ruby splits the axis.** `private` constrains the *call form*: no explicit receiver other than a literal `self` (since Ruby 2.7). A sibling receiver is still rejected, and `protected` exists precisely to permit `other.balance` between instances of the same class. - **Python** mangles `__bal` to `_Account__bal`: class-level by construction, and aimed at name collisions, not access control.

code

text · 8 lines
text
Java / C# / C++     other.balance   -> compiles; privacy is class-level
JavaScript          other.#bal      -> works if other carries #bal,
                                       else TypeError  (guard: #bal in other)
Smalltalk           (no syntax)     -> must send a message:  other balance
Ruby, private bal   other.bal       -> NoMethodError; only a literal `self`
                                       receiver is allowed (Ruby 2.7+)
Ruby, protected bal other.bal       -> allowed between same-class instances
Python, __bal       other.__bal     -> works: mangled to other._Account__bal

go deeper

for a junior

Recall that private normally means "only code inside this class", and that in Java or C# this lets one Account read another Account's private field. Knowing that not every language agrees is enough at this level.

for a middle

Name both camps with real languages and state each rule precisely: class body in Java/C#/C++, receiver's own methods in Smalltalk, no explicit receiver but a literal self in Ruby with protected as the fix, name mangling in Python.

for a senior

Connect the rule to design: class-level privacy is what lets equals, merge and diff live inside the type against the raw representation, while object-level privacy forces those operations into the published protocol. Mention JavaScript's run-time brand and the protected keyword clash between Ruby and Java.

for a principal

Frame it as three independent axes — whose code (class vs object), which relatives (inheritance), which compilation unit (package/module/assembly) — and reason about what each choice costs in coupling and evolvability: a published pairwise protocol is a contract you must keep, whereas a class-private one can be rewritten freely.

## The word "private" answers two different questions "Private" is supposed to answer *who may touch this?* — but there are two defensible answers, and languages have picked both. One answer is **this class's code**: any method written inside `Account` may read the representation of any `Account`, including one it was handed as an argument. The other answer is **this object**: a method may only reach the state of the object it is running on, and a sibling instance is as opaque as a stranger. Most people learn exactly one of these, from their first language, and mistake it for the definition of the word. ## Camp one: privacy belongs to the class Java, C#, C++ and TypeScript's compile-time `private` all scope access to the *class body* (in Java, to the whole top-level class including its nested classes). Inside a method of `Account`, the expression `other.balance` is legal for any `other` of static type `Account`. The consequence is structural, not cosmetic. A type can implement operations that involve two of its own instances — `equals`, `compareTo`, `merge`, `difference`, a copy constructor — entirely inside its own code, reading the other operand's representation directly. Nothing has to be published for the type to do its own work, so the public surface can stay as narrow as the *domain* requires rather than as wide as the *implementation* requires. ## JavaScript's twist: class-level privacy with a runtime brand JavaScript joined this camp with `#` private fields, and added a property none of the older languages have: the access is **branded** at run time. Inside the class body `other.#bal` works on any object that was constructed with that private name — so it is class-level — but on an object that was not, it throws a `TypeError` instead of quietly yielding `undefined`. That is why `#bal in other` exists: it is the standard, non-throwing way to ask "is this really one of mine?", and it doubles as a genuine identity check that no forger can imitate, because private names are not properties and cannot be created from outside the class body. ## Camp two: privacy belongs to the object Smalltalk takes the other side. Instance variables belong to the object; only the receiver's own methods can name them, and there is no syntax at all for reaching into a sibling. Every binary operation must send a message to the other object and work with whatever comes back. The result is a culture of small accessors and message passing, and a very clean substitutability story: a comparison written this way depends only on the other operand's protocol, never on its representation, so any object that answers the same messages can be passed in. The cost is that a type cannot keep a pairwise operation's data private — whatever the operation needs must be published in some form. ## Ruby: a rule about receivers, not about objects Ruby is the instructive case, because it looks like camp two and is not. Ruby's `private` constrains the **call form**, not the object graph: a private method may not be called with an explicit receiver *other than a literal `self`*. Since Ruby 2.7 (2019) `self.bal` is permitted for any private method — before 2.7 the only allowed explicit receiver was the setter form `self.foo = x` — while a receiver naming a *different* object is still rejected. So `other.bal` raises `NoMethodError` even though `other` is the same class. That reads like per-object privacy but is really a syntax rule about receivers. To get sibling access Ruby gives you `protected`, whose entire reason to exist is to allow `other.bal` from within the same class or a subclass. Note the trap: in Java and C#, `protected` means something else entirely — subclasses, plus the package in Java — so the same keyword carries incompatible meanings across the two ecosystems. ## Python: no answer at all Python has no access control. A single leading underscore is etiquette. A double leading underscore triggers **name mangling**: `self.__bal` inside `class Account` compiles to the attribute `_Account__bal`. Two things follow. First, the boundary is class-level by construction, because the same mangling applies to `other.__bal` written inside `Account`. Second, the feature's purpose is to stop a subclass accidentally colliding with a base class's attribute name — not to stop access, since anyone outside can write `acct._Account__bal`. ## What the choice changes in design - **Where pairwise operations live.** With class-level privacy, comparison, merging and diffing can be implemented against the raw representation inside the type. With object-level privacy those operations force a published protocol, so the representation choice leaks into the interface and becomes harder to change later. - **How wide the public surface gets.** Class-level privacy lets a type stay narrow; the Smalltalk answer trades a wider surface for stricter per-object isolation. - **The axis is not the inheritance axis.** Privacy answers *whose code*; `protected` answers *which relatives* (and in Ruby, *which receiver form*); module/package/assembly modifiers answer *which compilation unit*. Conflating them is the usual error. - **Compile time versus run time.** Java, C#, C++ and TypeScript check at compile time and nothing survives into the running program. JavaScript's `#` fields are checked at run time, which is why a foreign object raises an exception rather than returning `undefined`. ## Answering it in an interview Name both camps with concrete languages, state each rule precisely, then show the axis is independent of inheritance. Ruby's `protected` and Java's `protected` being different mechanisms with one spelling is the detail that proves you have actually worked in more than one of them.

  • Ruby and Java both have a keyword spelled `protected`. Do they mean the same thing?
    No. Java's `protected` grants access to subclasses and, additionally, to any class in the same package — it is about the inheritance and package axes. Ruby's `protected` is about the receiver axis: it permits calling the method with an explicit receiver from within the same class or a subclass, which is exactly the sibling access Ruby's `private` forbids. A Ruby programmer reaches for `protected` to write a comparison between two instances; a Java programmer never needs to, because Java's `private` already allows it.
  • If a type's privacy is object-level, how do you implement a comparison between two instances without leaking the representation?
    You publish a protocol rather than the fields: expose the smallest derived observation the comparison needs — an ordering key, a normalized value, or a `compareWith:` message the other object answers itself. Double dispatch is the common shape: ask the other object to compare itself against you, so each object reads only its own state. The representation stays hidden, but the fact that *some* comparable observation exists is now part of the public contract and you cannot silently drop it later.
  • JavaScript's `#` fields are checked at run time. What does that buy you that Java's compile-time `private` does not?
    An unforgeable identity check. Because private names are not properties and can only be introduced inside the declaring class body, `#bal in obj` proves the object really went through that class's constructor — a subclass or a look-alike duck cannot fake it. Java's `private` is erased after compilation, so the analogous check is an `instanceof` test that reflection and classloader tricks can work around; the tradeoff is that JavaScript's mistakes surface as run-time TypeErrors instead of compile errors.

Class-level privacy is a family safe: every member of the family knows the combination to any of the family's safes. Object-level privacy is a personal locker: you can open yours, and to learn what is in someone else's you have to ask them.

saying these in an interview costs you the question

  • Saying "private always means only this object" — the class-level rule in Java, C#, C++ and JavaScript is the majority answer, not the exception.
  • Claiming Ruby's `private` is per-object privacy. It is a rule about the call form: an explicit receiver other than a literal `self` is rejected, and `protected` re-enables sibling access.
  • Repeating the pre-2.7 Ruby rule that `self.foo` is only allowed for setters — since Ruby 2.7 a literal `self` receiver is legal for any private method.
  • Treating Python's `__name` as access control. It is name mangling to `_Class__name`, aimed at subclass name collisions; the attribute is trivially reachable from outside.
  • Assuming `other.#bal` returns `undefined` for a foreign object in JavaScript — it throws a TypeError, which is why `#bal in other` exists.
  • Collapsing the class-versus-object axis into the inheritance axis and answering the whole question with "that's what `protected` is for".

context