# Classes

Course: [Python](https://www.devtutorials.ai/learn/python) · Section: Object Oriented Programming
Source: https://www.devtutorials.ai/learn/python/oop/classes

> 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**.

```python runnable title="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.

```python runnable title="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.

```python runnable title="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.

```python runnable title="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:

```python runnable title="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.

```python runnable title="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.

```python runnable title="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.

```python runnable title="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.

```python runnable title="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__`.

```python runnable title="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.

```python runnable title="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.

```python runnable title="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.

```python runnable title="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.**

```python runnable title="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:

```python runnable title="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__`:

```python runnable title="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.

<div className="tscroll">

| | 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 |

</div>

## Methods

### What Are Methods?

A **method** is a function defined inside a class. It is the behaviour of the objects made from it.

```python runnable title="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.

```python runnable title="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

```python runnable title="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:

```python runnable title="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`.

```python runnable title="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.

```python runnable title="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.

```python runnable title="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:

```python runnable expect-error title="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.

```python runnable expect-error title="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.

```python runnable title="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.

```python runnable title="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.

```python runnable title="__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:

```python runnable title="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.

```python runnable title="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")))
```

```python runnable expect-error title="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.

```python runnable title="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:

```python runnable expect-error title="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.

```python runnable title="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.

```python runnable title="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.

```python runnable expect-error title="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`.

```python runnable title="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.

<div className="tscroll">

| 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 |

</div>

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

```python runnable title="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.

```python runnable title="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`.

```python runnable title="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.

```python runnable title="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

```python runnable title="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))
```

<div className="tscroll">

| | 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 |

</div>

## 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

```python runnable title="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.

```python runnable title="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.

```python runnable title="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:

```python runnable title="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

```python runnable title="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:

```python runnable expect-error title="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

```python runnable title="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:

```python runnable title="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`:

```python runnable title="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.

<div className="tscroll">

| 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 |

</div>

## 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**
