Classes in Python
By the Syncfusion DevTutorials team · Last updated
Classes and objects, attributes and methods, self, the constructor, access modifiers and name mangling, class and static methods, and Enum.
Think about a form the school office hands out.
The blank form has spaces for a name, a class and a set of marks. It is not about anybody. It is the shape that every student's record will take.
Fill one in for Asha and you have a record. Fill another for Ravi and you have a second one. Two records, same shape, different contents.
In Python the blank form is a class, and each filled-in copy is an object.
One shape, two records
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
asha = Student("Asha", 78)
ravi = Student("Ravi", 85)
print(asha.name, asha.marks)
print(ravi.name, ravi.marks)
One class was written. Two objects were made from it, and each one keeps its own name and marks.
A class is the shape. An object is one thing made from that shape, with its own values.
What You Will Learn
In this lesson, you will learn:
- What a class and an object are, and how to make one
- Where an object's data lives, and what belongs to the class instead
- How to give a class behaviour, and why every method starts with
self - What the constructor does
- How Python marks something as private, and why nothing is truly hidden
- Methods that belong to the class rather than to any one object
- How to write a fixed set of named values with
Enum
Classes and Objects
What Is a Class?
A class is a description of a kind of thing: what it knows, and what it can do.
It holds no data of its own. It is the form, not the filled-in copy.
A class on its own does nothing
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
print(Student)
print(type(Student))
The class exists now, but no Student objects have been created yet.
What Is an Object?
A class describes what something will look like. To actually store real data, you create objects from that class.
An object is one thing made from a class. Making one is called creating an instance, and the object is an instance of that class.
Making objects from the class
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
asha = Student("Asha", 78)
print(asha)
print(type(asha))
print(isinstance(asha, Student))
The default printout, <__main__.Student object at 0x...>, is Python saying "a Student, living at this place in memory". A later lesson makes that readable.
Creating Objects
You create an object by calling the class as though it were a function.
Three students, three objects
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
students = [Student("Asha", 78), Student("Ravi", 85), Student("Meera", 60)]
for student in students:
print(student.name, student.marks)
Each object keeps its own values. Changing one leaves the others alone:
Objects do not share their data
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
asha = Student("Asha", 78)
ravi = Student("Ravi", 85)
asha.marks = 90
print(asha.name, asha.marks)
print(ravi.name, ravi.marks)
Object References
A variable does not hold the object. It holds a reference to it — a way of reaching it. Two names can point at one object.
Two names, one object
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
asha = Student("Asha", 78)
same_student = asha
same_student.marks = 90
print(asha.marks)
print(asha is same_student)
Changing it through one name changes what you see through the other, because there is only one object. This is the same is versus == idea you met with lists.
Equal values are not the same object
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
a = Student("Asha", 78)
b = Student("Asha", 78)
print(a is b)
print(a == b)
Both are False. The two objects hold identical values, but they are separate objects, and Python compares objects by identity unless you teach it otherwise — which is a later lesson.
Attributes
What Are Attributes?
An attribute is a value attached to an object or a class. You reach it with a dot.
Reading and writing attributes
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
asha = Student("Asha", 78)
print(asha.name)
asha.marks = 85
print(asha.marks)
asha.section = "7B"
print(asha.section)
Python lets you add an attribute that was never in the class. That is flexible and dangerous: a typo creates a new attribute rather than raising.
A typo makes a new attribute
class Student:
def __init__(self, marks):
self.marks = marks
asha = Student(78)
asha.mrks = 85
print("marks:", asha.marks)
print("mrks:", asha.mrks)
print("Everything on this object:", vars(asha))
Nothing complained, and the real marks was never updated. vars() shows what an object actually holds, which is the quickest way to spot this.
Instance Variables
An instance variable belongs to one object. It is created with self.name = value, normally inside __init__.
Each object has its own
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
asha = Student("Asha", 78)
ravi = Student("Ravi", 85)
print(vars(asha))
print(vars(ravi))
Class Attributes
A class attribute is written directly in the class body. There is one of it, shared by every object.
One value, shared by all
class Student:
school = "Green Valley"
def __init__(self, name):
self.name = name
asha = Student("Asha")
ravi = Student("Ravi")
print(asha.school, ravi.school, Student.school)
Student.school = "Blue Hills"
print(asha.school, ravi.school)
Changing it on the class changed it for every object, because there is only one.
Class attributes are right for things that belong to the kind, not the individual: the school name, a pass mark, a counter of how many exist.
Counting the objects made
class Student:
count = 0
def __init__(self, name):
self.name = name
Student.count += 1
Student("Asha")
Student("Ravi")
Student("Meera")
print("Students created:", Student.count)
Object State
An object's state is all its attribute values at a moment. Methods are what change it.
State before and after
class Student:
def __init__(self, name):
self.name = name
self.marks = 0
self.attendance = 0
asha = Student("Asha")
print("Start:", vars(asha))
asha.marks = 78
asha.attendance = 92
print("Later:", vars(asha))
Class vs Instance Variables
The rule that catches everyone: assigning through an object creates an instance variable, hiding the class one.
Assigning through an object does not change the class
class Student:
school = "Green Valley"
def __init__(self, name):
self.name = name
asha = Student("Asha")
ravi = Student("Ravi")
asha.school = "Own School"
print("asha:", asha.school)
print("ravi:", ravi.school)
print("class:", Student.school)
print("asha's own attributes:", vars(asha))
asha now has its own school, which hides the shared one. ravi and the class are untouched.
Both kinds of variables may look similar when you use them, so it is worth seeing them side by side.
A mutable class attribute is the trap this creates:
A shared list, changed by everyone
class Student:
all_marks = []
def __init__(self, name):
self.name = name
def add_mark(self, mark):
self.all_marks.append(mark)
asha = Student("Asha")
ravi = Student("Ravi")
asha.add_mark(78)
ravi.add_mark(85)
print("asha sees:", asha.all_marks)
print("ravi sees:", ravi.all_marks)
append changes the one shared list rather than creating a new attribute, so both students see both marks. Give each object its own in __init__:
Each object gets its own list
class Student:
def __init__(self, name):
self.name = name
self.marks = []
def add_mark(self, mark):
self.marks.append(mark)
asha = Student("Asha")
ravi = Student("Ravi")
asha.add_mark(78)
ravi.add_mark(85)
print("asha:", asha.marks)
print("ravi:", ravi.marks)
Never put a list or a dictionary in the class body unless you really mean every object to share one. It is the same trap as a list default argument, and it is just as quiet.
| Class attribute | Instance variable | |
|---|---|---|
| Written | in the class body | as self.x = ... |
| How many | one, shared | one per object |
| Reached by | Student.x or obj.x | obj.x |
| Changing via the class | affects every object | not possible |
| Changing via an object | creates a new instance variable | changes that object |
| Use for | a pass mark, a counter, a shared name | a name, marks, anything individual |
Methods
What Are Methods?
A method is a function defined inside a class. It is the behaviour of the objects made from it.
A class with behaviour
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
def has_passed(self):
return self.marks >= 35
asha = Student("Asha", 78)
ravi = Student("Ravi", 20)
print(asha.has_passed())
print(ravi.has_passed())
The same method gave two answers, because it read each object's own marks.
Instance Methods
An instance method works on one object. Its first parameter is always self, which is that object.
Several methods on one class
class Student:
PASS_MARK = 35
def __init__(self, name, marks):
self.name = name
self.marks = marks
def has_passed(self):
return self.marks >= self.PASS_MARK
def grade(self):
if self.marks >= 80:
return "A"
if self.marks >= 60:
return "B"
return "C"
def summary(self):
return f"{self.name}: {self.marks} ({self.grade()})"
print(Student("Asha", 78).summary())
print(Student("Meera", 92).summary())
summary() called grade() through self. Methods reach each other the same way they reach attributes.
Calling Methods
Two ways to call the same method
class Student:
def __init__(self, name):
self.name = name
def greet(self):
return f"Hello, {self.name}"
asha = Student("Asha")
print(asha.greet())
print(Student.greet(asha))
asha.greet() is the normal way, and Python turns it into the second form, passing the object as self. Seeing that once makes self much less mysterious.
Forgetting the brackets gives you the method itself:
With brackets and without
class Student:
def __init__(self, name):
self.name = name
def greet(self):
return f"Hello, {self.name}"
asha = Student("Asha")
print(asha.greet)
print(asha.greet())
Method Parameters
Parameters come after self.
Methods that take values
class Student:
def __init__(self, name):
self.name = name
self.marks = []
def add_mark(self, mark):
self.marks.append(mark)
def add_many(self, *marks):
self.marks.extend(marks)
def best(self, count=1):
return sorted(self.marks, reverse=True)[:count]
asha = Student("Asha")
asha.add_mark(78)
asha.add_many(85, 60, 92)
print(asha.marks)
print(asha.best())
print(asha.best(2))
Everything you learned about arguments applies: defaults, *args, keyword-only parameters.
Returning Values
A method that changes the object usually returns nothing. One that answers a question returns the answer.
Changing against answering
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
def raise_by(self, points):
self.marks += points
def percentage(self):
return self.marks / 100 * 100
asha = Student("Asha", 78)
result = asha.raise_by(5)
print("raise_by returned:", result)
print("but the object changed:", asha.marks)
print("percentage returned:", asha.percentage())
raise_by gave back None, like every function with no return. The work it did is in the object.
The self Keyword
Introduction to self
self is the object the method was called on. Python passes it automatically.
It is not a keyword — it is just the conventional name of the first parameter. Calling it something else works and confuses every reader, so do not.
self is the object itself
class Student:
def __init__(self, name):
self.name = name
def who_am_i(self):
return self
asha = Student("Asha")
print(asha.who_am_i() is asha)
Leaving it out breaks the method:
A method without self
class Student:
def __init__(self, name):
self.name = name
def greet():
return "Hello"
Student("Asha").greet()
TypeError: greet() takes 0 positional arguments but 1 was given — Python passed the object, and the method had nowhere to put it.
Accessing Attributes
Inside a method, every attribute needs self. in front of it.
A bare name is not an attribute
class Student:
def __init__(self, marks):
self.marks = marks
def has_passed(self):
return marks >= 35
Student(78).has_passed()
NameError: name 'marks' is not defined. Inside the method, marks is looked for among local names and globals — never among the object's attributes. The editor marks this one as you type.
With self in front
class Student:
def __init__(self, marks):
self.marks = marks
def has_passed(self):
return self.marks >= 35
print(Student(78).has_passed())
Accessing Methods
One method calls another through self too.
Methods calling methods
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
def grade(self):
return "A" if self.marks >= 80 else "B"
def report(self):
return f"{self.name} - {self.marks} - {self.grade()}"
print(Student("Meera", 92).report())
Constructors
What Is a Constructor?
A constructor is the method Python runs automatically when an object is created. Its job is to set the object's starting state.
__init__()
The constructor is called __init__. The two underscores on each side mark it as a method Python calls for you rather than one you call yourself.
__init__ runs on creation
class Student:
def __init__(self, name):
print("Setting up", name)
self.name = name
asha = Student("Asha")
print("Done:", asha.name)
You never call __init__ yourself. Student("Asha") does it.
A class without one still works — the object simply starts empty:
No constructor, no attributes
class Student:
pass
asha = Student()
print(vars(asha))
asha.name = "Asha"
print(vars(asha))
Constructor Parameters
Everything after self is what the caller must supply.
Required and optional parameters
class Student:
def __init__(self, name, marks=0, section="7A"):
self.name = name
self.marks = marks
self.section = section
print(vars(Student("Asha")))
print(vars(Student("Ravi", 85)))
print(vars(Student("Meera", 92, "7B")))
print(vars(Student("Ken", section="8A")))
A missing required value
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
Student("Asha")
Object Initialization
A constructor can work things out, not just copy values in.
A constructor that does some work
class Report:
def __init__(self, name, marks):
self.name = name
self.marks = marks
self.total = sum(marks)
self.average = round(self.total / len(marks), 1)
self.passed = self.average >= 35
report = Report("Asha", [78, 85, 60])
print(vars(report))
It can also refuse bad values, which stops a broken object existing at all:
A constructor that refuses
class Student:
def __init__(self, name, marks):
if not 0 <= marks <= 100:
raise ValueError("marks must be between 0 and 100")
self.name = name
self.marks = marks
print(Student("Asha", 78).marks)
print(Student("Ravi", 150).marks)
The first student was built; the second was refused. Checking in the constructor means every object that exists is valid.
Access Modifiers
Python has no private keyword. It uses naming conventions instead, and trusts programmers to follow them.
Public Members
No underscore means public: anyone may use it. This is the default and the right choice most of the time.
Public attributes
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
asha = Student("Asha", 78)
print(asha.name, asha.marks)
Protected Members
One leading underscore means "internal — please do not touch from outside". Python does not enforce it at all.
A single underscore is a request, not a rule
class Student:
def __init__(self, name, marks):
self.name = name
self._marks = marks
def report(self):
return f"{self.name}: {self._marks}"
asha = Student("Asha", 78)
print(asha.report())
print(asha._marks)
asha._marks = 999
print(asha.report())
Nothing stopped the outside world, and nothing warned. The underscore is a message to other programmers: this may change, do not depend on it.
Private Members
Two leading underscores ask Python to do something real.
A double underscore is hidden from outside
class Student:
def __init__(self, name, marks):
self.name = name
self.__marks = marks
def report(self):
return f"{self.name}: {self.__marks}"
asha = Student("Asha", 78)
print(asha.report())
print(asha.__marks)
AttributeError: 'Student' object has no attribute '__marks' — from outside, the name does not exist.
Name Mangling
It is not really hidden. Python renamed it, a process called name mangling: inside the class, __marks becomes _Student__marks.
Where the attribute actually went
class Student:
def __init__(self, marks):
self.__marks = marks
asha = Student(78)
print(vars(asha))
print(asha._Student__marks)
So a determined caller can still reach it. The point of mangling is not security — it is to stop a subclass accidentally using the same attribute name as its parent.
| Written as | Means | Enforced? |
|---|---|---|
marks | public, use freely | — |
_marks | internal, please leave alone | no, by convention only |
__marks | private, renamed to _Class__marks | partly — hidden, not secured |
Python's view is that privacy is a convention between programmers, not a lock. Start public, use one underscore for things that are nobody else's business, and keep two for the rare case where a subclass might clash.
Class Methods
What Are Class Methods?
A class method belongs to the class rather than to any one object. It receives the class as its first argument instead of an object.
The @classmethod Decorator
A method that takes the class
class Student:
school = "Green Valley"
count = 0
def __init__(self, name):
self.name = name
Student.count += 1
@classmethod
def how_many(cls):
return f"{cls.count} students at {cls.school}"
Student("Asha")
Student("Ravi")
print(Student.how_many())
@classmethod above the def is a decorator — a line that changes how the function below it works.
The cls Parameter
cls is to a class method what self is to an instance method: the first argument, passed automatically. It holds the class, not an object.
cls is the class
class Student:
@classmethod
def what_am_i(cls):
return cls
print(Student.what_am_i() is Student)
Accessing Class Attributes
Class methods read and change class attributes through cls.
Changing a class attribute properly
class Student:
school = "Green Valley"
@classmethod
def rename_school(cls, new_name):
cls.school = new_name
print(Student.school)
Student.rename_school("Blue Hills")
print(Student.school)
Creating Class Methods
Their most common real use is as an alternative constructor: another way to build an object.
A second way to make a Student
class Student:
def __init__(self, name, marks):
self.name = name
self.marks = marks
@classmethod
def from_text(cls, line):
name, marks = line.split(",")
return cls(name.strip(), int(marks))
def __str__(self):
return f"{self.name}: {self.marks}"
print(Student("Asha", 78))
print(Student.from_text("Ravi, 85"))
from_text builds and returns an object, using cls rather than naming Student. That matters for subclasses: cls is whichever class it was called on.
Class Methods vs Instance Methods
The three kinds side by side
class Student:
school = "Green Valley"
def __init__(self, name, marks):
self.name = name
self.marks = marks
def report(self):
return f"instance method - knows this student: {self.name}"
@classmethod
def info(cls):
return f"class method - knows the class: {cls.school}"
@staticmethod
def has_passed(marks):
return f"static method - knows neither: {marks >= 35}"
asha = Student("Asha", 78)
print(asha.report())
print(Student.info())
print(Student.has_passed(78))
| Instance method | Class method | Static method | |
|---|---|---|---|
| First argument | self, the object | cls, the class | none |
| Can read instance data | yes | no | no |
| Can read class data | yes | yes | no |
| Called on | an object | the class or an object | the class or an object |
| Use for | behaviour of one object | counters, alternative constructors | a helper that belongs here by subject |
Static Methods
What Are Static Methods?
A static method is a plain function that lives inside a class. It gets no self and no cls, and knows nothing about either.
The @staticmethod Decorator
A helper kept with the class it belongs to
class Student:
PASS_MARK = 35
def __init__(self, name, marks):
self.name = name
self.marks = marks
@staticmethod
def is_valid_mark(mark):
return 0 <= mark <= 100
print(Student.is_valid_mark(78))
print(Student.is_valid_mark(150))
print(Student("Asha", 78).is_valid_mark(85))
It works called on the class or on an object, because it ignores both.
Creating Static Methods
Use one when a function is clearly about the class's subject but needs nothing from any object.
Validation helpers, kept where they belong
class Student:
def __init__(self, name, marks):
if not self.is_valid_mark(marks):
raise ValueError("bad mark")
self.name = self.clean_name(name)
self.marks = marks
@staticmethod
def is_valid_mark(mark):
return isinstance(mark, int) and 0 <= mark <= 100
@staticmethod
def clean_name(name):
return name.strip().title()
asha = Student(" asha sharma ", 78)
print(repr(asha.name), asha.marks)
Static Methods vs Instance Methods
A static method that never gets used by the class is just a function in a strange place. The test is simple: if it does not mention the class's subject, move it out.
Why not just a function?
def is_valid_mark_function(mark):
return 0 <= mark <= 100
class Student:
@staticmethod
def is_valid_mark(mark):
return 0 <= mark <= 100
print(is_valid_mark_function(78))
print(Student.is_valid_mark(78))
Both work. The static method says this belongs to Student, which is worth something when a file holds several classes.
Enums
What Are Enums?
An enum is a fixed set of named values. Use one when a value may only be one of a few known options.
Real-World Examples
A grade is A, B, C or F. A term is 1, 2 or 3. A submission is draft, submitted or marked. None of these should ever hold "sumbitted".
Without an enum, nothing stops a typo:
Plain text lets a typo through
status = "sumbitted"
if status == "submitted":
print("Marking it")
else:
print("Not marked - status is", status)
No error, no warning, and the work never gets marked.
How to Declare Enums
A fixed set of named values
from enum import Enum
class Grade(Enum):
A = 80
B = 60
C = 35
F = 0
print(Grade.A)
print(Grade.A.name)
print(Grade.A.value)
Each member has a name (what you typed) and a value (what you set it to).
A wrong name now fails loudly:
A name that is not a member
from enum import Enum
class Status(Enum):
DRAFT = 1
SUBMITTED = 2
print(Status.SUMBITTED)
How to Use Enums
Comparing, looping and looking up
from enum import Enum
class Grade(Enum):
A = 80
B = 60
C = 35
top = Grade.A
print(top == Grade.A)
print(top == Grade.B)
for grade in Grade:
print(grade.name, "needs", grade.value)
print(Grade(60))
print(Grade["C"])
An enum member is not equal to its value, which is deliberate:
A member is not its value
from enum import Enum
class Grade(Enum):
A = 80
print(Grade.A == 80)
print(Grade.A.value == 80)
Use .value when you need the number. The member itself stands for the idea, not the number.
Enums work well with match:
An enum in a match statement
from enum import Enum
class Status(Enum):
DRAFT = 1
SUBMITTED = 2
MARKED = 3
status = Status.SUBMITTED
match status:
case Status.DRAFT:
print("Still being written")
case Status.SUBMITTED:
print("Waiting to be marked")
case Status.MARKED:
print("Finished")
A dotted name in a case is compared rather than captured, which is exactly the rule you met in the pattern matching lesson.
Reach for an enum whenever you catch yourself comparing against the same handful of strings in several places. A typo then becomes an error instead of a silent wrong answer.
Common mistakes
Each row is one mistake. The middle column is what you will actually see on screen.
| Mistake | What you see | Why it happens | Fix |
|---|---|---|---|
Forgetting self in a method definition | TypeError: greet() takes 0 positional arguments but 1 was given | Python always passes the object | def greet(self): |
| Using a bare attribute name inside a method | NameError: name 'marks' is not defined | Attributes are only reached through self | self.marks |
| A list or dict as a class attribute | Every object shares one | It is created once, in the class body | Create it in __init__ |
| Assigning through an object to change a class attribute | The class never changes | Assignment makes an instance variable | Assign on the class, or use a class method |
| A typo in an attribute name | A new attribute, silently | Python allows new attributes any time | Check with vars(obj) |
Calling __init__ yourself | Confusing results | Python calls it when you create the object | Student("Asha") |
Expecting __marks to be secure | _Student__marks is readable | Name mangling renames, it does not lock | Treat it as a convention |
| Comparing an enum member to its value | Grade.A == 80 is False | A member stands for the idea, not the number | Compare .value, or compare members |
Expecting two equal objects to be == | False for identical data | Objects compare by identity by default | Define __eq__, which is a later lesson |
| A method that returns nothing being assigned | The variable is None | Methods that change state return None | Call it on its own line |
What to Remember
- A class is the shape; an object is one thing made from it, with its own values.
- A variable holds a reference to an object, so two names can point at one object.
- An instance variable is set with
self.x = ...and belongs to one object. - A class attribute is written in the class body and is shared by every object.
- Assigning through an object creates an instance variable that hides the class one.
- Never put a list or dictionary in the class body unless every object really should share it.
- Every instance method takes
selffirst, and every attribute inside it needsself.in front. __init__is the constructor. Python calls it when you create the object; you never call it yourself.- One underscore means "internal, please leave alone". Two trigger name mangling to
_Class__name. @classmethodtakesclsand is for counters and alternative constructors;@staticmethodtakes neither.- An
Enumis a fixed set of named values, and a member is not equal to its value — use.value.
Next: Inheritance