Back to Blog
Python

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.

Pythonpropertiesencapsulationobject-oriented programmingdata validation
Illustration of a Python property decorator controlling access to an attribute, showing the getter and setter flow.

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

ApproachCall siteValidationTypical use
Plain attributeobj.attrNoneInternal data with no invariants
Propertyobj.attrIn setterPublic API requiring validation or computed values
Explicit methodsobj.get_attr()In methodRare 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.

python getter setter: Practical Usage and Code Examples | RYUSLOG DEV