Lesson 4 of 5

Classes & OOP

Python has classes, constructors, inheritance, and even a rough equivalent of interfaces — the ideas from C#'s object model all carry over. What's different is how much of it the language enforces versus leaves as a social convention. C#'s private is a compiler-enforced wall. Python's version is a naming convention that a determined caller can walk straight through.

💡 The Bottom Line

Python classes use __init__ instead of a same-named constructor, self instead of an implicit this (but written explicitly as the first parameter of every method), and a single leading underscore instead of the private keyword — one that the language doesn't actually enforce.

Defining a class and a constructor

C#
public class Dog
{
    public string Name { get; set; }

    public Dog(string name)
    {
        Name = name;
    }

    public void Bark()
    {
        Console.WriteLine($"{Name} says woof!");
    }
}

var d = new Dog("Rex");
d.Bark();
Python
class Dog:
    def __init__(self, name):
        self.name = name

    def bark(self):
        print(f"{self.name} says woof!")

d = Dog("Rex")
d.bark()

📋 Key differences

  • The constructor is a method named __init__, not a method matching the class name.
  • Every instance method explicitly takes self as its first parameter — Python's this, but never implicit and never omitted from the signature.
  • No new keyword when creating an instance: Dog("Rex"), not new Dog("Rex").
  • No separate property syntax ({ get; set; }) — you just assign self.name directly; Python has a separate @property decorator for when you actually need getter/setter logic, used far less often than C# properties.

⚠️ Gotcha

Forgetting self as the first parameter of a method is the single most common Python OOP mistake for C# developers — it's invisible in C# because this is always implicit there. Miss it in Python and you'll get a confusing TypeError: bark() takes 0 positional arguments but 1 was given — Python is telling you it tried to pass the instance in as self automatically, but your method signature didn't leave room for it.

"Private" fields: convention, not enforcement

C#
public class Account
{
    private decimal _balance;
    // truly inaccessible outside the class
}
Python
class Account:
    def __init__(self):
        self._balance = 0
        # a single underscore signals "internal use
        # only" — but nothing stops account._balance

A single leading underscore (_balance) is the community convention for "treat this as private." A double leading underscore (__balance) triggers Python's name mangling, which makes accidental access harder but still not truly impossible. Neither is enforced by the language the way C#'s private keyword is — Python trusts other developers to respect the underscore, rather than blocking them at compile time.

Inheritance and interfaces

Inheritance looks familiar — class Puppy(Dog): instead of class Puppy : Dog — and calling the parent constructor uses super().__init__(...) instead of base(...). Python has no dedicated interface keyword; the closest equivalent is an abstract base class from the abc module, or — more commonly in idiomatic Python — just relying on "duck typing": if an object has the right methods, it's usable, regardless of any declared type relationship at all.

🙋 There Are No Dumb Questions

Q: If there's no real "private," how does anyone build safe abstractions in Python?
A: Mostly through convention, documentation, and code review, rather than compiler enforcement — Python's design philosophy leans toward "we're all consenting adults here." For cases where you genuinely need enforcement (e.g. a public library boundary), Python developers reach for double-underscore name mangling or, increasingly, static type checkers — but true compile-time privacy the way C# has it doesn't exist.

🔧 Try It Yourself

Port a small C# class hierarchy you know well — a base class with one virtual method and one subclass overriding it — into Python. Use super().__init__(...) in the subclass's __init__, and confirm calling the overridden method on a subclass instance calls the subclass's version, exactly like C#'s virtual/override — except in Python, every method is overridable by default, with no virtual keyword required.

✅ Quick Check

In Python, what actually enforces that a field named _balance is treated as private?


Last stop: a rundown of the gotchas that catch C# developers most often once they're writing real Python day to day.

← Back to
Next →