When external code needs to read or write a private property, it does so through getter and setter methods — public methods dedicated to accessing a single property in a controlled way.
class Person {
private age
function getAge() {
return this.age
}
function setAge(value) {
if value < 0 or value > 150:
throw Error("Age must be realistic")
this.age = value
}
}
p = new Person()
p.setAge(30) // goes through validation
print(p.getAge()) // 30
p.setAge(-5) // throws an error — invalid state is prevented
Compare this to making age public directly — any code anywhere could set person.age = -5 with no validation at all. Getters and setters turn a simple property assignment into an enforceable rule.
Not every property needs both. Some are read-only from the outside (only a getter — e.g. a computed fullName derived from firstName and lastName), and some might be write-only in rare cases (only a setter — e.g. a password, which should never be readable back out in plain form).
A caveat worth knowing: writing a getter and setter for every single property, with no actual logic inside them, doesn't add real encapsulation — it's just extra boilerplate around a public property. True encapsulation value comes from the validation, computation, or side effects a setter or getter can add. Modern languages increasingly offer shortcuts for the trivial case (like PHP's property hooks, C#'s auto-properties, or Python's @property decorator) while still allowing full custom logic when it's actually needed.