Python Getter Setter: Using @property Correctly
python getter setter: Learn how Python implements getter and setter behavior with @property, when to use it, and how it differs from Java-style accessor methods.
Python doesn't have private fields in the way Java or C# do. There is no private keyword and no enforced access control at the language level. When developers search for "python getter setter", what they are usually looking for is the idiomatic way to control attribute access, and that answer is the @property decorator rather than a pair of get_xxx() and set_xxx() methods.
The Encapsulation Problem in Python
In Java, encapsulation is enforced by the compiler:
private int age; public int getAge() { return age; } public void setAge(int age) { this.age = age; }
Python has no equivalent enforcement. An attribute is just an entry in the instance's __dict__:
class Person: def __init__(self, age): self.age = age
Any code can read or write person.age directly. The convention is that a leading underscore marks an attribute as internal:
class Person: def __init__(self, age): self._age = age
That is a convention, not a rule. If you need actual control over what happens when an attribute is read or written, you need either explicit methods or a property.
Traditional Getter and Setter Methods
The most literal translation of the Java pattern uses explicit methods:
class Person: def __init__(self, age): self._age = age def get_age(self): return self._age def set_age(self, value): if value < 0: raise ValueError("Age cannot be negative") self._age = value
This works, but it changes the call site. Instead of person.age, you write person.get_age(). If you later add validation, every place that reads person.age directly must be updated to call the method. That is a real maintenance cost, and it is why explicit getter/setter methods are considered non-idiomatic in Python.
The @property Decorator Approach
The idiomatic Python solution is @property. It defines a method that behaves like a plain attribute at the call site:
class Person: def __init__(self, age): self._age = age @property def age(self): return self._age @age.setter def age(self, value): if value < 0: raise ValueError("Age cannot be negative") self._age = value
Now person.age reads through the getter, and person.age = 25 invokes the setter. The call site looks identical to a plain attribute, but the behavior is fully controlled. You can also define a deleter with @age.deleter.
The key advantage is backward compatibility: you can start with a plain attribute and later replace it with a property without changing any code that reads or writes it. The public API stays stable while the internal behavior gains validation or computed logic.
Adding Validation and Derived Behavior
The setter is where validation belongs. Consider a Temperature class:
class Temperature: def __init__(self, celsius): self.celsius = celsius @property def celsius(self): return self._celsius @celsius.setter def celsius(self, value): if value < -273.15: raise ValueError("Temperature below absolute zero") self._celsius = value @property def fahrenheit(self): return self._celsius * 9 / 5 + 32
The fahrenheit property is read-only because it has no setter. Assigning temp.fahrenheit = 100 raises AttributeError: can't set attribute. This is a clean way to expose derived values without letting callers corrupt the underlying state. The constructor calls the setter, so validation runs at initialization time as well as on every later assignment.
Performance and Runtime Cost
Every property access is a method call. A plain attribute lookup is a dictionary access on the instance's __dict__. A property access goes through the descriptor protocol and adds function-call overhead. For most application code the difference is negligible, but in a hot loop that reads an attribute millions of times it can show up in profiling.
If profiling shows property access is a bottleneck, the practical options are:
- Cache the computed value in the setter and return the cached value from the getter.
- Drop the property and use a plain attribute, performing validation once at construction.
- Use
__slots__to reduce per-instance memory and speed up attribute access.
Do not optimize prematurely. Measure first. The encapsulation and validation benefits of properties usually outweigh the small overhead.
Choosing Between Plain Attributes, Properties, and Explicit Methods
| Approach | Call site | Validation | Typical use |
|---|---|---|---|
| Plain attribute | obj.attr | None | Internal data with no invariants |
| Property | obj.attr | In setter | Public API requiring validation or computed values |
| Explicit methods | obj.get_attr() | In method | Rare in modern Python; mostly ported code |
Use a plain attribute when the field is free-form and no invariant must be enforced. Use a property when you need validation, derived values, or lazy initialization. Explicit get_xxx() and set_xxx() methods are generally non-idiomatic in Python and are mainly seen in code migrated from Java or in frameworks that require that style.
Maintaining Compatibility When Refactoring
The most practical benefit of properties appears during refactoring. Suppose Person starts with a plain attribute:
class Person: def __init__(self, name): self.name = name
Everywhere in the codebase, person.name is read and written directly. A new requirement arrives: names must be stripped of whitespace and cannot be empty. With a property, you add the logic without touching any call sites:
class Person: def __init__(self, name): self.name = name @property def name(self): return self._name @name.setter def name(self, value): value = value.strip() if not value: raise ValueError("Name cannot be empty") self._name = value
The constructor already routes through the setter, so validation applies at initialization too. This is why properties are the default choice for public attributes in Python classes.
One caveat: subclassing a class that uses properties requires care. If you override the property, you must override the getter and setter together. To call the parent setter, access the property object directly:
class Employee(Person): @property def name(self): return super().name.upper() @name.setter def name(self, value): Person.name.fset(self, value)
Person.name.fset is the original setter function stored on the property object. Calling it with self applies the parent's validation logic to the subclass instance. This is the one place where properties add noticeable complexity, so if you expect deep inheritance hierarchies, weigh whether a property is the right abstraction or whether a different design would serve better.