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 attributeInstance variable
Writtenin the class bodyas self.x = ...
How manyone, sharedone per object
Reached byStudent.x or obj.xobj.x
Changing via the classaffects every objectnot possible
Changing via an objectcreates a new instance variablechanges that object
Use fora pass mark, a counter, a shared namea 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 asMeansEnforced?
markspublic, use freely—
_marksinternal, please leave aloneno, by convention only
__marksprivate, renamed to _Class__markspartly — 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 methodClass methodStatic method
First argumentself, the objectcls, the classnone
Can read instance datayesnono
Can read class datayesyesno
Called onan objectthe class or an objectthe class or an object
Use forbehaviour of one objectcounters, alternative constructorsa 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.

MistakeWhat you seeWhy it happensFix
Forgetting self in a method definitionTypeError: greet() takes 0 positional arguments but 1 was givenPython always passes the objectdef greet(self):
Using a bare attribute name inside a methodNameError: name 'marks' is not definedAttributes are only reached through selfself.marks
A list or dict as a class attributeEvery object shares oneIt is created once, in the class bodyCreate it in __init__
Assigning through an object to change a class attributeThe class never changesAssignment makes an instance variableAssign on the class, or use a class method
A typo in an attribute nameA new attribute, silentlyPython allows new attributes any timeCheck with vars(obj)
Calling __init__ yourselfConfusing resultsPython calls it when you create the objectStudent("Asha")
Expecting __marks to be secure_Student__marks is readableName mangling renames, it does not lockTreat it as a convention
Comparing an enum member to its valueGrade.A == 80 is FalseA member stands for the idea, not the numberCompare .value, or compare members
Expecting two equal objects to be ==False for identical dataObjects compare by identity by defaultDefine __eq__, which is a later lesson
A method that returns nothing being assignedThe variable is NoneMethods that change state return NoneCall 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 self first, and every attribute inside it needs self. 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.
  • @classmethod takes cls and is for counters and alternative constructors; @staticmethod takes neither.
  • An Enum is a fixed set of named values, and a member is not equal to its value — use .value.

Next: Inheritance