Classes & OOP

Defining classes (OOP design)

The "Helper functions," "Object equality," and "Type hints" sections all assume you can write a class — this is the foundation under them. LC design problems (caches, tries, rate limiters, iterators) are graded on clean class structure, so this matters directly for the FDE coding round.

Anatomy

class Trie:
    def __init__(self):           # constructor — runs automatically on Trie()
        self.root = TrieNode()    # instance attribute, stored on the object

    def insert(self, word):       # method — first parameter is always `self`
        node = self.root          # reach attributes through self.
        ...

Instance vs class attributes

class Counter:
    total_made = 0                # CLASS attribute — one copy shared by ALL instances
    def __init__(self, start):
        self.value = start        # INSTANCE attribute — separate per object
        Counter.total_made += 1

Composition — classes holding other classes

The common design shape: a public container class holds instances of an internal helper class. TrieTrieNode is the canonical example.

class TrieNode:                   # internal building block
    def __init__(self):
        self.children = {}        # {char: TrieNode} — holds more of its own kind
        self.is_end = False

class Trie:                       # public API the caller uses
    def __init__(self):
        self.root = TrieNode()

The _private convention

Python has no enforced access control. A leading underscore is a convention meaning "internal, not part of the public contract":

class Trie:
    def insert(self, word): ...   # public API
    def _walk(self, prefix):      # internal helper — `_` signals private
        ...

Nothing stops outside code from calling _walk, but the underscore tells readers (and tooling) not to. Use it for helpers that aren't part of the class's public surface.

Interview shape

For a design problem the graded structure is: __init__ sets up state, each public operation is one short single-purpose method, internal helpers are _-prefixed. Match the method names the problem gives you exactly (insert, search, startsWith — not add/find). Inheritance and abstract base classes (ABC, @abstractmethod, super().__init__()) build on this — covered at the Cache ABC exercise.

Helper functions (nested vs class method)

Both work for tree-recursion / repeated-logic helpers. Nested is slightly more pythonic for one-off helpers; class method for shared / OOP-heavy problems.

# NESTED with CLOSURE + LIST ACCUMULATOR — no nonlocal needed (mutation)
class Solution:
    def someMethod(self, root):
        result = []                          # captured by closure
        def dfs(node):
            if not node: return
            result.append(node.val)          # in-place mutation
            dfs(node.left)
            dfs(node.right)
        dfs(root)
        return result


# NESTED with CLOSURE + SCALAR ACCUMULATOR — needs nonlocal
class Solution:
    def someMethod(self, root):
        best = 0
        def dfs(node):
            nonlocal best                    # required to reassign outer scalar
            if not node: return 0
            l, r = dfs(node.left), dfs(node.right)
            best = max(best, l + r + node.val)
            return node.val + max(l, r)
        dfs(root)
        return best


# INNER WITH PARAMETERS — same syntax as any function
def outer():
    multiplier = 3                           # captured by closure
    def scale(x):                            # x is a normal parameter
        return x * multiplier                # uses param + captured value
    print(scale(5))                          # 15


# PARAMETERS SHADOW outer names — pure local
def outer():
    x = "outer"
    def inner(x):                            # parameter x, scoped to inner only
        print(x)                             # whatever was passed
    inner("argument")                        # "argument"
    print(x)                                 # "outer" — untouched


# RECURSIVE INNER — parameter changes per call, closure carries shared state
def outer(root):
    out = []
    def dfs(node):                           # `node` differs per call
        if not node: return
        out.append(node.val)
        dfs(node.left)
        dfs(node.right)
    dfs(root)
    return out


# CLASS METHOD ALTERNATIVE — helper as regular method on Solution
class Solution:
    def someMethod(self, root):
        return self._dfs(root)
    def _dfs(self, node):
        if not node:
            return 0
        return 1 + max(self._dfs(node.left), self._dfs(node.right))
# Underscore prefix `_dfs` is convention for "internal helper, not public API"

Object equality (== vs is, custom __eq__)

== checks equality; is checks identity (same memory address). For built-in types Python provides sensible == semantics. For user-defined classes the default == falls back to identity — same as is.

# BUILT-INS — sensible default equality (element-wise, value-based)
[1, 2, 3] == [1, 2, 3]            # True
(1, 2) == (1, 2)                  # True
{1, 2} == {2, 1}                  # True (sets are unordered)
{"a": 1} == {"a": 1}              # True
"abc" == "abc"                    # True

# USER CLASSES — default `==` is identity (same memory) unless you override
class Foo:
    def __init__(self, x):
        self.x = x

a = Foo(1)
b = Foo(1)
a == b                            # False — different objects, no __eq__ defined
a is b                            # False
a == a                            # True — same object


# CUSTOM EQUALITY — define __eq__
class Foo:
    def __init__(self, x):
        self.x = x
    def __eq__(self, other):
        return isinstance(other, Foo) and self.x == other.x
    def __hash__(self):
        return hash(self.x)       # define if you want hashability (use as dict key or in a set)

a = Foo(1)
b = Foo(1)
a == b                            # True — same x value


# LEETCODE TIP — TreeNode and ListNode (as LC provides them) have NO __eq__
# so `node1 == node2` is identity comparison. This is exactly what you want
# for LC 236 (LCA) and similar: "is this the same node as p?"

Two rules of thumb:

Type hints

Python is dynamically typed. Type annotations are optional and not enforced at runtime — they're purely for readability and static checkers (mypy, IDE autocomplete).

def add(a, b):                    # works, types inferred at runtime
    return a + b

def add(a: int, b: int) -> int:   # purely advisory; behavior identical
    return a + b

LC convention: keep type hints on the top-level method (LC provides them in boilerplate), drop them on helper functions to keep code tight.

class Solution:
    def maxDepth(self, root: Optional[TreeNode]) -> int:    # hints kept
        def dfs(node):                                       # hints dropped
            if not node: return 0
            return 1 + max(dfs(node.left), dfs(node.right))
        return dfs(root)