Naming Conventions
Python does not have true private members like Java or C++. Instead, it uses naming conventions to signal access levels. Developers follow these conventions to indicate which parts of a class are internal implementation details and which are the public interface.
Convention Over Enforcement
Python trusts developers to follow conventions. The underscore prefixes are signals to other programmers, not security barriers. "We are all consenting adults here" is a common Python philosophy.
Why it matters: Understanding these conventions helps you write clear APIs and avoid accidentally breaking code when using third-party libraries.
Access Level Naming Conventions
name
_name
__name
self.owner = "Alice"Public: anyone can access
self._balance = 1000Protected: internal use
self.__pin = "1234"Private: name mangled
Public Attributes
Attributes without underscores are part of the public API. Users of your class are expected to access and modify them freely.
class Person:
def __init__(self, name, age):
self.name = name # Public
self.age = age # Public
person = Person("Alice", 30)
print(person.name) # Output: Alice
person.age = 31 # Direct modification is OK
print(person.age) # Output: 31
Public attributes are the intended interface. They can be read and written directly without any restrictions.
Protected Attributes (Single Underscore)
A single leading underscore signals "internal use only." It is a convention that tells other developers to not access this directly from outside the class or subclasses.
class Employee:
def __init__(self, name, salary):
self.name = name
self._salary = salary # Protected: internal
def get_annual_salary(self):
return self._salary * 12
emp = Employee("Bob", 5000)
print(emp._salary) # Works, but not recommended!
print(emp.get_annual_salary()) # Use public methods instead
Python allows access to _salary, but the underscore says "this is an implementation detail that may change." Use the public method instead.
Private Attributes (Double Underscore)
Double leading underscores trigger name mangling. Python renames __attr to _ClassName__attr, making accidental access harder.
class BankAccount:
def __init__(self, owner, pin):
self.owner = owner
self.__pin = pin # Private: name mangled
def verify_pin(self, pin):
return self.__pin == pin
acc = BankAccount("Alice", "1234")
# print(acc.__pin) # AttributeError!
print(acc._BankAccount__pin) # Works but DO NOT do this!
print(acc.verify_pin("1234")) # Correct way: True
Name mangling prevents accidental access and name clashes in inheritance. It is not true security since you can still access it with the mangled name.
Practice: Naming Conventions
Task: Create a User class with public username, protected _email, and private __password attributes.
Show Solution
class User:
def __init__(self, username, email, password):
self.username = username # Public
self._email = email # Protected
self.__password = password # Private
def check_password(self, pwd):
return self.__password == pwd
user = User("alice", "alice@mail.com", "secret123")
print(user.username) # alice
print(user.check_password("secret123")) # True
Task: Create a Secret class with __code attribute. Try accessing it directly (will fail) then using the mangled name.
Show Solution
class Secret:
def __init__(self, code):
self.__code = code
s = Secret("ABC123")
# This will raise AttributeError:
# print(s.__code)
# This works (but don't do this in real code!):
print(s._Secret__code) # Output: ABC123
Task: Create Vehicle with protected _speed. Create Car that has a method using _speed. Show that subclass can access protected attributes.
Show Solution
class Vehicle:
def __init__(self, speed):
self._speed = speed # Protected
class Car(Vehicle):
def describe(self):
return f"Car moving at {self._speed} mph"
car = Car(60)
print(car.describe()) # Car moving at 60 mph
# Protected is accessible to subclasses
Property Decorator
The @property decorator lets you define methods that behave like attributes. You can add logic to getting, setting, or deleting values while maintaining a clean attribute-like syntax. This is Python's way of creating getters and setters.
Property Decorator Flow
user.name
def name(self)
return self._name
user.name = "Bob"
def name(self, value)
self._name = value
del user.name
def name(self)
del self._name
Basic Property (Getter)
Use @property to create a read-only computed attribute. The method runs when you access the attribute but looks like a simple attribute access.
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return 3.14159 * self._radius ** 2
@property
def radius(self):
return self._radius
c = Circle(5)
print(c.radius) # 5 (no parentheses!)
print(c.area) # 78.53975 (computed on access)
Properties are accessed without parentheses, just like attributes. The area is computed fresh each time you access it.
Setter Property
Use @property_name.setter to allow assignment. The setter method receives the new value and can process or validate it before storing.
class Person:
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value.strip().title()
p = Person(" alice ")
print(p.name) # Output: Alice
p.name = "bob smith"
print(p.name) # Output: Bob Smith
The setter automatically cleans and formats the name. Users assign values normally while your processing happens behind the scenes.
Read-Only Properties
Create properties without setters to make computed or protected values read-only. Attempting to assign will raise an AttributeError.
class Rectangle:
def __init__(self, width, height):
self._width = width
self._height = height
@property
def area(self):
return self._width * self._height
rect = Rectangle(4, 5)
print(rect.area) # 20
# rect.area = 100 # AttributeError: can't set attribute
Without a setter, the property is read-only. This prevents users from directly modifying computed values that should derive from other attributes.
Practice: Property Decorator
Task: Create a Temperature class with _celsius attribute. Add a fahrenheit property that computes and returns the Fahrenheit value.
Show Solution
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
temp = Temperature(25)
print(temp.fahrenheit) # Output: 77.0
Task: Extend the Temperature class so you can set fahrenheit and it updates _celsius internally.
Show Solution
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5/9
temp = Temperature(0)
temp.fahrenheit = 212
print(temp._celsius) # Output: 100.0
Task: Create Person with first_name and last_name. Add a full_name property (getter) and setter that splits the full name into first and last.
Show Solution
class Person:
def __init__(self, first, last):
self.first_name = first
self.last_name = last
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
@full_name.setter
def full_name(self, value):
parts = value.split()
self.first_name = parts[0]
self.last_name = parts[-1]
p = Person("John", "Doe")
p.full_name = "Jane Smith"
print(p.first_name, p.last_name) # Jane Smith
Data Validation
Properties shine when you need to validate or constrain data. The setter can check values before storing them, raising exceptions or correcting invalid input. This ensures your objects always remain in a valid state.
Validating with Exceptions
Raise ValueError in the setter when the input is invalid. This immediately signals the problem to the caller.
class Product:
def __init__(self, name, price):
self.name = name
self.price = price # Uses the setter
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if value < 0:
raise ValueError("Price cannot be negative")
self._price = value
p = Product("Phone", 999)
# p.price = -50 # Raises ValueError!
Note that __init__ uses self.price (not self._price), so the setter validation runs during object creation too.
Type Checking
Validate that the value is the expected type before storing. This catches type errors early.
class Student:
def __init__(self, name, grade):
self.name = name
self.grade = grade
@property
def grade(self):
return self._grade
@grade.setter
def grade(self, value):
if not isinstance(value, int):
raise TypeError("Grade must be an integer")
if not 0 <= value <= 100:
raise ValueError("Grade must be 0-100")
self._grade = value
s = Student("Alice", 95)
# s.grade = "A" # Raises TypeError!
Multiple validation checks ensure the value meets all requirements before being stored.
Auto-Correction in Setters
Instead of raising errors, you can silently correct or constrain values. Use this when correction is safe and expected.
class Volume:
def __init__(self, level):
self.level = level
@property
def level(self):
return self._level
@level.setter
def level(self, value):
# Clamp value between 0 and 100
self._level = max(0, min(100, value))
v = Volume(50)
v.level = 150
print(v.level) # Output: 100 (clamped)
v.level = -20
print(v.level) # Output: 0 (clamped)
This pattern is common for UI controls like sliders where exceeding bounds should just clamp to the limit, not crash.
Practice: Data Validation
Task: Create a Circle class with radius property. The setter should raise ValueError if radius is not positive.
Show Solution
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("Radius must be positive")
self._radius = value
c = Circle(5)
# c.radius = -1 # ValueError: Radius must be positive
Task: Create a Username class with a name property. Validate that name is between 3 and 20 characters.
Show Solution
class Username:
def __init__(self, name):
self.name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not 3 <= len(value) <= 20:
raise ValueError("Name must be 3-20 chars")
self._name = value
u = Username("alice")
# u.name = "ab" # ValueError!
Task: Create a Contact class with email property. Validate that email contains "@" and "." characters.
Show Solution
class Contact:
def __init__(self, email):
self.email = email
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if "@" not in value or "." not in value:
raise ValueError("Invalid email format")
self._email = value.lower()
c = Contact("Alice@Mail.COM")
print(c.email) # Output: alice@mail.com
Task: Create a Progress class with percent property. Auto-clamp values to 0-100 range instead of raising errors.
Show Solution
class Progress:
def __init__(self, percent=0):
self.percent = percent
@property
def percent(self):
return self._percent
@percent.setter
def percent(self, value):
self._percent = max(0, min(100, value))
p = Progress()
p.percent = 150
print(p.percent) # 100 (clamped)
Task: Create BankAccount with balance property. Validate non-negative on set. Add deposit() and withdraw() methods that use the property.
Show Solution
class BankAccount:
def __init__(self, balance=0):
self.balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("Balance cannot be negative")
self._balance = value
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
self.balance -= amount # Validation happens here!
acc = BankAccount(100)
acc.withdraw(50)
# acc.withdraw(100) # ValueError!
Task: Create User with password property. Validate: min 8 chars, has digit, has uppercase. Store hashed (just use len for demo).
Show Solution
class User:
def __init__(self, username, password):
self.username = username
self.password = password
@property
def password(self):
return self._hashed
@password.setter
def password(self, value):
if len(value) < 8:
raise ValueError("Min 8 characters")
if not any(c.isdigit() for c in value):
raise ValueError("Must contain a digit")
if not any(c.isupper() for c in value):
raise ValueError("Must contain uppercase")
self._hashed = f"hashed_{len(value)}"
u = User("alice", "Secret123")
print(u.password) # hashed_9
Key Takeaways
Public by Default
Python attributes without underscores are public. This is your class's intended interface for external use.
Single Underscore = Internal
Prefix with _ to signal "internal use only." It is a convention that other developers should respect.
Double Underscore = Mangled
Prefix with __ to trigger name mangling. Python renames it to _ClassName__attr to prevent accidental access.
@property for Getters
Use @property to create computed attributes or controlled access with attribute-like syntax.
@name.setter for Setters
Add a setter to enable assignment with validation or transformation logic behind the scenes.
Validate Early
Use setters to validate data at the point of assignment. Keep objects in a valid state at all times.
Knowledge Check
Quick Quiz
Test what you've learned about Python encapsulation and properties