Object-Oriented Programming

Classes and Objects: Instance and Class Variables

7 min read

You've been using objects all along

Every value you've worked with so far — strings, lists, dictionaries — is already an object. That's why "hello".upper() works: a string isn't just text, it's a bundle of data (the characters) plus behavior (methods like .upper() and .split()) packaged together. Object-oriented programming (OOP) is simply the practice of building your own bundles like that, instead of only using the ones Python ships with.

The tool for defining a new bundle is the class keyword.


What is a class?

A class is a blueprint that defines what a certain kind of object looks like — what data it holds and what it can do. It doesn't hold any real data itself; it just describes the shape.

class className():
    # statements

Use class, followed by a name (by convention, capitalized), and a colon. The body can contain variables, functions, or — for now — nothing at all:

class Person():
    pass

pass is a placeholder meaning "no body yet" — it lets you define an empty class that's still valid Python.


What is an object?

An object is a specific instance built from a class — the thing the blueprint describes, actually constructed. Call the class like a function to create one:

class Person():
    pass

James = Person()
Sarah = Person()

print(James)
print(Sarah)

Output:

<__main__.Person object at 0x11f864890>
<__main__.Person object at 0x11f864910>

Two calls to Person() produced two separate objects — you can tell because they printed at two different memory addresses. Each is independently a Person, but they're not the same object, the same way two houses built from the same blueprint aren't the same house.


Instance variables

An instance variable is a piece of data that belongs to one specific object, not to the class in general. You can attach one just by assigning to it with dot notation:

class Person():
    pass

James = Person()
Sarah = Person()

James.salary = 3000
James.empid = 12
James.address = 'Seattle'

Sarah.address = 'Austin'
Sarah.designation = 'Manager'

print(James.salary)      # 3000
print(James.empid)       # 12
print(Sarah.designation) # Manager

Output:

3000
12
Manager

Notice James and Sarah don't even have the same attributes — James has salary and empid, Sarah has designation, and only address is set on both. This works, and it's a useful way to see that instance variables are genuinely per-object. But assigning attributes ad hoc like this, from outside the class, isn't how real code is written — later in this lesson you'll see the standard way, using __init__, which guarantees every object gets its attributes set up front instead of leaving them optional and scattered.


Class variables

A class variable is defined inside the class body (not inside a method) and is shared by every object built from that class — one copy, not one per object:

class Person():
    Company = 'Google'
    pass

James = Person()
Sarah = Person()

Both objects can read it the same way, through either the class or the instance:

print(Person.Company)   # Google
print(James.Company)    # Google
print(Sarah.Company)    # Google

Output:

Google
Google
Google

Here's the part that trips people up. Watch what happens when you assign to James.Company directly:

James.Company = 'Apple'

print(James.Company)   # Apple
print(Sarah.Company)   # Google
print(Person.Company)  # Google

Output:

Apple
Google
Google

James.Company = 'Apple' did not change the shared class variable — it created a brand-new instance variable on James that happens to have the same name, which now shadows the class variable whenever you look it up through James. Sarah and Person itself never see it; they still read the original 'Google'. To actually change the value every object shares, you have to assign to the class itself: Person.Company = 'Apple' would update it everywhere at once, because there's genuinely only one copy.

This is the core distinction to hold onto: instance variables are private to one object; class variables are shared — until an instance variable with the same name shadows them for that one object.

Diagram — A Class Is a Blueprint, Objects Are What Gets Built

class Person (blueprint)Company = 'Google'shared class variable__init__(self, ...)builds each objectJames = Person(...)Sarah = Person(...)James (object)designation = 'Software Engineer'address = 'Seattle'own instance variables —not shared with SarahSarah (object)designation = 'Manager'address = 'Austin'own instance variables —not shared with JamesBoth objects still read the same Person.Company unless one of them is given its own copy.

Seeing what's actually stored: __dict__

Every object and every class keeps its attributes in a dictionary you can inspect directly, __dict__:

print(James.__dict__)
print(Sarah.__dict__)

Output:

{'salary': 3000, 'empid': 21, 'address': 'Seattle', 'Company': 'Apple'}
{'address': 'Austin', 'designation': 'Manager'}

This confirms exactly what happened above: James.__dict__ contains its own 'Company': 'Apple' entry, while Sarah.__dict__ has no Company key at all — it still falls back to reading Person.Company because it doesn't have its own.


__init__: setting up an object properly

Attaching attributes one at a time from outside the class, like James.salary = 3000, is fragile — nothing forces every object to actually get a salary. The standard fix is __init__, a special method Python calls automatically every time you create a new object:

class Car:
    # initializer
    def __init__(self):
        pass

__init__() runs the moment an object is constructed, which makes it the natural place to set up whatever attributes that object needs from the start. Its first parameter is always self — a reference to the object being built:

class Person():
    Company = 'Google'

    def __init__(self, designation, address):
        self.designation = designation
        self.address = address

James = Person('Software Engineer', 'Seattle')
Sarah = Person('Manager', 'Austin')

print(James.designation)  # Software Engineer
print(Sarah.address)      # Austin

Output:

Software Engineer
Austin

Now every Person is guaranteed to have designation and address set the moment it's created — there's no way to end up with a Person missing one, the way Sarah was missing salary earlier. The arguments you pass to Person(...) are handed straight to __init__, right after self.

  • The first argument of every method is a reference to the current instance
  • By convention, that argument is named self (Python doesn't require the name, but every codebase uses it)
  • Inside __init__, self refers to the object currently being constructed
  • In other methods, self refers to whichever instance the method was called on
  • If you've used Java or C++, self plays the same role as this — Python just makes it an explicit parameter instead of an implicit keyword

Instance methods

A method defined inside a class, taking self as its first parameter, is called an instance method — it can read (and change) the data that belongs to the specific object it's called on:

class Person():
    Company = 'Google'

    def __init__(self, designation, address):
        self.designation = designation
        self.address = address

    def printdesc(self):
        print(self.designation)
        print(self.address)

James = Person('Software Engineer', 'Seattle')
Sarah = Person('Manager', 'Austin')

Sarah.printdesc()
James.printdesc()

Output:

Manager
Austin
Software Engineer
Seattle

Sarah.printdesc() and James.printdesc() run the exact same code, but self is bound to a different object each time — that's the whole mechanism behind why the two calls print different results.


Key takeaway

A class is a blueprint; an object is a specific instance built from it. Instance variables belong to one object and are typically set up inside __init__, whose first parameter, self, is a reference to the object being built or acted on. Class variables are defined in the class body and shared by every instance — unless an instance variable with the same name shadows it for that one object. __dict__ lets you inspect exactly what's stored on any object or class.


What's next?

__init__ and printdesc above are both instance methods — the default kind, bound to one object. Next: Instance, Class, and Static Methods covers the other two kinds Python supports, plus the built-in dunder methods (like __str__) that let your objects control how they print and behave.