Various styles for managing or accessing your attributes/properties of your class in Python

Serdar A.
9 min readFeb 23, 2025

--

It is an important issue to directly or indirectly access,modify or share, etc. the attributes/properties of the classes you create in Python. You can do/manage attributes/properties with different approaches.

Let’s look at these step by step.

In this article contains:

  • attributes vs properties
  • public / non-public properties
  • Encapsulation
  • Getter/Setter methods
  • @property decorator
  • Descriptor

What is attribute and property? What is differences?

Attributes in Python

Attributes are defined by the data variables such as name, age, width, height, etc.

There is 2 types in Python

  1. Class attributes
  • This attribute is created inside class.
  • Shared with other methods/objects inside class.

Example:

class TestObject:

# class attribute
variable = 10

# define a method
def increment(self):
TestObject.variable += 1

t1 = TestObject() # t1 instance
t2 = TestObject(); # t2 instance

print(t1.variable)
print(t2.variable)

t1.increment()

print(t1.variable)
print(t2.variable)

Output is

10
10
11
11 # t2 and t1 object share variable attribute

2. Instance attributes

  • Instance Attributes are unique to each instance
  • Every object/instance contains its attributes and can be changed without modifying other instances

Example :

class Example:

mycount = 2 # class attrİbute

def __init__(self,total): # total instance attrİbute
self.total = total

def method1(self):
return self.total + self.mycount


e1 = Example(10)
e2 = Example(20)

print(e1.method1())
print(e2.method1())

Output is :

12
22

Properties in Python

Properties are unique attributes that contain getter, setter, and deleter methods such as __get__, __set__, and __delete__ methods.

Example :

# create a class
class ExampleClass:

# constructor
def __init__(self, variable):
self._variable = variable # non-public

# getting the variable
@property
def variable(self):
print('Getting variable')
return self._variable

# setting the variable
@variable.setter
def variable(self, variable):
print('Setting variable to ' + variable)
self._variable = variable

# deleting the variable
@variable.deleter
def variable(self):
print('Deleting variable')
del self._variable


e1 = ExampleClass("test")
print(e1.variable)

e1.variable = "change text"
print(e1.variable)

Output is

Getting variable
test
Setting variable to change text
Getting variable
change text

Various styles for managing or accessing your attributes/properties

  1. Direct access

For example in below class, name and age are properties.In constructor set values, later in showInfo print values of properties.

class ExampleClass:

# constructor
def __init__(self, name, age):
self.name = name
self.age = age

def showInfo(self):
print("Name : {} \nAge: {} ".format(self.name,self.age))

e1 = ExampleClass("Test",30)
print(e1.showInfo())

Output is :

Name : Test 
Age: 30

In this example you can direct access/modify your properties

e1.age = 40
e1.name = "change text"
print(e1.showInfo())

Output is :

Name : change text 
Age: 40

2. Encapsulation

Now we change our class and we define properties as non-public with “_” or “__” keyword

  • Encapsulation with “_” keyword
class ExampleClass:

# constructor
def __init__(self, name, age):
self._name = name # _name is non public prop
self._age = age # _age is non public prop

def setName(self, name): # setter method for non-public _name prop
self._name = name

def setAge(self,age): # setter method for non-public _age prop
self._age = age

def showInfo(self):
print("Name : {} \nAge: {} ".format(self._name,self._age))

If you direct access like :

 e1 = ExampleClass("Test",30)
print(e1.age) # direct acccess

You get error :

print(e1.age)
^^^^^^
AttributeError: 'ExampleClass' object has no attribute 'age'

Modify value of property like :

e1.age = 40
print(e1.showInfo())

Output is :

Name: Test
Age: 30

can not change age properties.

If you use setter method for age property, you can modify value of age property.

e1.setAge(40)
print(e1.showInfo())

Output is :

Name: Test
Age: 40

You can modify value of _age property. In actually, your properties are _name and _age

 e1._age = 40
print(e1.showInfo())

Output is :

Name: Test
Age: 40
  • Encapsulation with __ keyword

For example :

class Employee:
def __init__(self):
self.__name = "default" # not override instance properties
self.__age = 0

def changename(self, name):
self.__name = name

def getname(self):
return self.__name

def __assignName__(self, name):
self.__name = name

Test it :

e1 = Employee()
print(e1.getname())

Output is :

default

If you want to direct access property

print(e1.__name) 

You get error:

print(e1.__name)
^^^^^^^^^
AttributeError: 'Employee' object has no attribute '__name'

If you override property with direct access like :

e1__name = "change text"
print(e1.getname())

Output is :

default

not overriding.

If you override value;

e1.changename("change text")     
print(e1.getname())

e1.__assignName__("assign text")
print(e1.getname())

Output is :

change text    # override
assign text # override

3. Getter & Setter

In here, value assign to setter method of property instead of assigning to property in __init__ method. getter method return value of non-public property.

For example :

class ExampleClass:
def __init__(self, x, y):
self.set_x(x) # move to setter method set_x
self.set_y(y) # move to setter method set_y

def get_x(self):
return self._x # get_x return non-public _x prop

def set_x(self, x):
self._x = self.validate(x) # change behavior of method. check validate with validate methods

def get_y(self):
return self._y

def set_y(self, y):
self._y = self.validate(y)

def validate(self, value):
if not isinstance(value, int | float):
raise ValueError("properties must be numbers")
return value

If you direct access like :

p1 = ExampleClass(1,2)
p1.x

You get error:

AttributeError: 'ExampleClass' object has no attribute 'x'.

Note :

  • If you write code like :
def __init__(self, v1, v2):
self.v1 = v1
self.v2 = v2

Your properties are public and access directly:

classInstance.prop
  • If you write code like :
def __init__(self,v1,v2):
self._v1 = v1
self._v2 = v2

Your properties are non-public and can not access directly :

classInstance.prop  

You get error like :

AttributeError: 'xxxClass' object has no attribute 'xxxProperties'.

4. @property decorator

This decorator is used

  • managing to access
  • adding to extra behaviour (like validation)
  • managing to getter and setter methods
  • read-only or write-only features
  • lazy load process

etc.

In shortly,

You don’t want to write getter / setter method to access your properties and also you don’t want to direct access your properties.

You can use @property decorator in 2 ways:

  • via property(….)

For example :

class Point:
def __init__(self, x, y):
self._x = x
self._y = y

def get_x(self):
print("Getter x")
return self._x

def set_x(self, value):
print("Setter x")
self._x = value

def get_y(self):
print("Getter y")
return self._y

def set_y(self, value):
print("Setter y")
self._y = value

def del_x(self):
print("Delete x")
del self._x

def del_y(self):
print("Delete y")
del self._y

x = property(
fget=get_x,
fset=set_x,
fdel=del_x,
doc="x properties"
)
y = property(
fget=get_y,
fset=set_y,
fdel=del_y,
doc="y properties"
)

x and y properties are non-public. we defined setter, getter and deleter methods and inside property(), we defined fget for getter method, fset for setter method, fdel for deleter method and doc is for documentation.

Test it :

pointCls = Point(2,5)     # init values for properties
print(pointCls.x)

Output is :

 Getter x
2

Now, you may think direct access for x properties. But in actually, Python runs get_x method in background.

 pointCls.x = 7            # modify              
print(pointCls.x)

Output is :

Setter x
Getter x
7

you may think direct set new value for x properties. But in actually, Python runs set_x method in background.

You can use @property decorator instead of get_prop, set_prop, del_prop for every properties. And also you don’t need to write property(….) definitation.

For example :

class Point:
def __init__(self, x, y):
self._x = x
self._y = y

@property
def x(self):
"""The x property."""
print("Get x")
return self._x

@x.setter
def x(self, value):
print("Set x")
self._x = value

@property
def y(self):
"""The y property."""
print("Get y")
return self._y

@y.setter
def y(self, value):
print("Set y")
self._y = value

@x.deleter
def x(self):
print("Delete x")
del self._x

@y.deleter
def y(self):
print("Delete y")
del self._y
  • define property with @property keyword.
  • @x.setter is setter for x property
  • @x.deleter is deleter for x property

Test it :

pointCls = Point(2, 5)
print(pointCls.x)

Output is :

Get x
2
 pointCls.x = 7    # modify
print(pointCls.x)

Output is :

Set x
Get x
7

Note : If you don’t need extra behaviour for your properties, you don’t need use this style. Because this style causes extra complexities for other team-mates developers. In addition, causes extra CPU consumption. This is slower.

5. Read-only & Write-only

  • Read-only

If you remove setter methods or throw exception for setter method, only use @property methods. You make object read-only

For example :

class Point:
def __init__(self, x, y):
self._x = x
self._y = y

@property
def x(self):
"""The x property."""
print("Get x")
return self._x

@property
def y(self):
"""The y property."""
print("Get y")
return self._y

If you want, you can write exception for setter

     class WriteCoordinateError(Exception):
pass


class Point:
def __init__(self, x, y):
self._x = x
self._y = y

@property
def x(self):
"""The x property."""
print("Get x")
return self._x

@x.setter
def x(self, value):
raise WriteCoordinateError("x coordinate is read-only")

@property
def y(self):
"""The y property."""
print("Get y")
return self._y

Test it :

p1 = Point(2,5)
p1.x = 7

You get error :

WriteCoordinateError: x coordinate is read-only
  • Write-only
   @property
def x(self):
raise AttributeError("x prop is write-only")
class Point:
def __init__(self, x, y):
self._x = x
self._y = y

@property
def x(self):
raise AttributeError("x prop is write-only")

@x.setter
def x(self, value):
print("Set x")
self._x = value

@property
def y(self):
"""The y property."""
print("Get y")
return self._y

Test it :

p1 = Point(2,5)
p1.x = 7

print(p1.x)

You get error when reading value

AttributeError: x prop is write-only.

6. Descriptor

You can use same operations/processes for different properties or add extra features/behaviours for getter, setter and deleter operations.

  • Non-data descriptor (Read-only descriptor)
class ReadOnly_attribute():                          # read-only descriptor
def __get__(self, obj, type=None) -> object:
print("accessing the attribute to get the value")
return 42
def __set__(self, obj, value) -> None:
print("accessing the attribute to set the value")
raise AttributeError("Cannot change the value")
# object definitation
class TestObject():
attribute1 = ReadOnly_attribute()

Test it

myObj = TestObject()
print(myObj.attribute1)

Output is :

accessing the attribute to get the value
42

Modify attribute1

myObj.attribute1 = 10

You get error :

 raise AttributeError("Cannot change the value")
AttributeError: Cannot change the value

Same operation/behaviour example:

For example, I have a object (ExampleObject)

  from datetime import date

class ExampleObject:
def __init__(self, name, start_date, end_date):
self.name = name
self.start_date = start_date
self.end_date = end_date

@property
def name(self):
return self._name

@name.setter
def name(self, value):
self._name = value.upper()

@property
def start_date(self):
return self._start_date

@start_date.setter
def start_date(self, value):
self._start_date = date.fromisoformat(value)

@property
def end_date(self):
return self._end_date

@end_date.setter
def end_date(self, value):
self._end_date = date.fromisoformat(value)

we defined 2 properties with @property : start_date and end_date property. We write same date format code in both setter method. Now we can refactor this code blocks via descriptor.

      from datetime import date, datetime, timedelta


class DateDescriptor:
def __set_name__(self, owner, name):
self._name = name

def __get__(self, instance, owner):
return instance.__dict__[self._name]

def __set__(self, instance, value):
instance.__dict__[self._name] = date.fromisoformat(value)


class TestObject:
start_date = DateDescriptor()
end_date = DateDescriptor()

def __init__(self, name, start_date, end_date):
self.name = name
self.start_date = start_date
self.end_date = end_date

@property
def name(self):
return self._name

@name.setter
def name(self, value):
self._name = value.upper()

DateDescriptor is a descriptor. start_date and end_date properties are defined as DateDescriptor. __set__ methods format value.

In general, Three methods are defined in a descriptor. If you want to define just one of them. If you want to define extra methods.

  • __get__(self, instance, owner). Accesses the attribute. It returns the value
  • __set__(self, instance, value). Sets the attribute. Does not return anything
  • __delete__(self, instance). Deletes the attribute. Does not return anything

Note : Python descriptors are instantiated just once per class. That means that every single instance of a class containing a descriptor shares that descriptor instance.

For example :

class MyNumericDescriptor():
def __init__(self):
self.value = 0

def __get__(self, obj, type=None) -> object:
return self.value

def __set__(self, obj, value) -> None:
if value > 9 or value < 0 or int(value) != value:
raise AttributeError("The value is invalid")
self.value = value


class TestObject():
number = MyNumericDescriptor()

I create two instance from TestObject

     myObj1 = TestObject()  # instance myObj1
myObj2 = TestObject() # instance myObj2

myObj1.number = 3 # modify myObj1's prop

print(myObj1.number)
print(myObj2.number)

Output is :

3   #share
3 #share

Create another instance

myObj3 = TestObject()
print(myObj3.number)

Output is :

3

Now change value of number property of myObj3 instance

myObj3.number = 5   # modify myObj3 instance prop value

Test it :

print(myObj1.number)     # myObj1                                               
print(myObj2.number) # myObj2
print(myObj3.number) # myObj3

Output is :

5
5
5

How to fix it?

You can use dictionary

class MyNumericDescriptor():

def __init__(self):
self.value = {} # dictionary

def __get__(self, obj, type=None) -> object:
try:
return self.value[obj] # get data from dict
except:
return 0

def __set__(self, obj, value) -> None:
if value > 9 or value < 0 or int(value) != value:
raise AttributeError("The value is invalid")
self.value[obj] = value # set to dict
class TestObject():
number = MyNumericDescriptor()

Test it

    myObj1 = TestObject()   # instance myObj1
myObj2 = TestObject() # instance myObj2

myObj1.number = 3 # modify myObj1 instance prop value
print(myObj1.number)
print(myObj2.number)

Output is :

3
0

Create another instance

myObj3 = TestObject()
print(myObj3.number)

Output is :

0

Now change value of number property of myObj3 instance

    myObj3.number = 8   # modify myObj3 instance prop value


print(myObj1.number) # myObj1 instance
print(myObj2.number) # myObj2 instance
print(myObj3.number) # myObj3 instance

Output is :

3
0
8

You can use different styles to

  • manage
  • access control
  • adding extra features/behaviours
  • validation

for your properties of your classes.

In this article, I want to show you these styles.

I hope it was useful for you

--

--

Serdar A.
Serdar A.

Written by Serdar A.

Senior Software Developer & Architect at Havelsan Github: https://github.com/serdaralkancode #Java & #Spring & #BigData & #React & #Microservice

No responses yet