Python Classes vs. JavaScript Classes/Prototypes

Week 3: Monday Afternoon Session

Introduction: From JavaScript to Python Classes

Welcome JavaScript developers!

As a JavaScript developer transitioning to Python, you'll find both similarities and key differences in how these languages implement object-oriented programming. While JavaScript has evolved to include class syntax in ES6+, it's still prototype-based under the hood. Python, on the other hand, has had class-based OOP as a core feature since its inception. Understanding these differences will help you become a more effective Python developer while leveraging your existing JavaScript knowledge.

Mental model shift: Think of JavaScript as retrofitting class-like behavior onto its prototype system, whereas Python was designed from the ground up with classes in mind. JavaScript's class syntax is syntactic sugar over its prototype mechanism, while Python's classes are fundamental to the language design.

Basic Class Definition: Syntax Comparison

Let's start by comparing the basic syntax for defining classes in both languages:

JavaScript Class (ES6+)

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
  }
  
  // Static method
  static createAnonymous() {
    return new Person('Anonymous', 0);
  }
}

Python Class

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def greet(self):
        return f"Hello, my name is {self.name} and I am {self.age} years old."
    
    # Static method
    @staticmethod
    def create_anonymous():
        return Person('Anonymous', 0)

Key syntax differences to note:

The explicit self parameter: Perhaps the most striking difference is Python's explicit self parameter. While JavaScript implicitly provides this in methods, Python requires you to explicitly declare and use self. This makes the code more explicit and avoids some of the confusion around this binding in JavaScript.

Creating and Using Objects

The process of instantiating objects and calling methods is quite similar in both languages, with some subtle differences:

JavaScript

// Creating objects
const alice = new Person('Alice', 30);
const anonymous = Person.createAnonymous();

// Using objects
console.log(alice.name);  // Alice
console.log(alice.greet());  // Hello, my name is Alice and I am 30 years old.
console.log(anonymous.greet());  // Hello, my name is Anonymous and I am 0 years old.

Python

# Creating objects
alice = Person('Alice', 30)
anonymous = Person.create_anonymous()

# Using objects
print(alice.name)  # Alice
print(alice.greet())  # Hello, my name is Alice and I am 30 years old.
print(anonymous.greet())  # Hello, my name is Anonymous and I am 0 years old.

Notable similarities and differences:

No "new" keyword in Python: In JavaScript, forgetting the new keyword can lead to subtle bugs. Python doesn't use a special keyword for instantiation - you simply call the class as if it were a function. This design choice eliminates an entire class of potential errors.

Constructors and Initialization

Constructors work differently in Python and JavaScript:

JavaScript Constructor

class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
    this.created = new Date();
    
    // Constructor can contain logic
    if (price < 0) {
      throw new Error("Price cannot be negative");
    }
    
    // Constructor implicitly returns the new object
  }
}

Python Constructor

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price
        
        import datetime
        self.created = datetime.datetime.now()
        
        # __init__ can contain logic
        if price < 0:
            raise ValueError("Price cannot be negative")
        
        # __init__ should not return anything

Key differences:

Two-phase initialization in Python: In Python, object creation is actually a two-step process: __new__ creates the object, and __init__ initializes it. As a beginner, you'll rarely need to override __new__, but it's helpful to understand that __init__ doesn't actually create the object—it just sets up an already-created object.

Default argument values are handled similarly in both languages, but with different syntax:

JavaScript Default Parameters

class User {
  constructor(username, isAdmin = false, level = 1) {
    this.username = username;
    this.isAdmin = isAdmin;
    this.level = level;
  }
}

Python Default Parameters

class User:
    def __init__(self, username, is_admin = False, level = 1):
        self.username = username
        self.is_admin = is_admin
        self.level = level

Caution with mutable defaults in Python: One key difference not shown above is that Python has a gotcha with mutable default arguments. If you use a mutable object (like a list or dictionary) as a default parameter value, it's created once when the function is defined, not each time the function is called. This can lead to unexpected behavior:

# This can cause problems
class User:
    def __init__(self, username, roles=[]):  # This list is shared across all instances!
        self.username = username
        self.roles = roles

# Correct approach
class User:
    def __init__(self, username, roles=None):
        self.username = username
        self.roles = roles if roles is not None else []

Properties and Access Control

JavaScript and Python have different approaches to properties and access control:

JavaScript Getters and Setters

class Circle {
  constructor(radius) {
    this._radius = radius;  // Convention: underscore for "private"
  }
  
  // Getter
  get radius() {
    return this._radius;
  }
  
  // Setter with validation
  set radius(value) {
    if (value <= 0) {
      throw new Error("Radius must be positive");
    }
    this._radius = value;
  }
  
  // Computed property
  get area() {
    return Math.PI * this._radius * this._radius;
  }
}

Python Properties

class Circle:
    def __init__(self, radius):
        self._radius = radius  # Convention: underscore for "private"
    
    # Property getter
    @property
    def radius(self):
        return self._radius
    
    # Property setter with validation
    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("Radius must be positive")
        self._radius = value
    
    # Computed property
    @property
    def area(self):
        import math
        return math.pi * self._radius * self._radius

Using these properties is similar in both languages:

JavaScript

const circle = new Circle(5);
console.log(circle.radius);  // 5 (calls the getter)
console.log(circle.area);    // 78.54... (calls the getter)

circle.radius = 10;          // Calls the setter
console.log(circle.area);    // 314.16... (calls the getter)

try {
  circle.radius = -1;        // Throws an error
} catch (e) {
  console.error(e.message);  // "Radius must be positive"
}

Python

circle = Circle(5)
print(circle.radius)  # 5 (calls the getter)
print(circle.area)    # 78.54... (calls the getter)

circle.radius = 10    # Calls the setter
print(circle.area)    # 314.16... (calls the getter)

try:
    circle.radius = -1  # Raises an error
except ValueError as e:
    print(e)  # "Radius must be positive"

Key differences in property systems:

Python's privacy model: Python follows a "we're all consenting adults here" philosophy. Instead of strict access control, it uses conventions (like leading underscores for private attributes) and documentation to indicate how things should be used. There's a name mangling feature for attributes starting with double underscores (e.g., __attr), but it's more to prevent accidental name collisions in inheritance than to enforce privacy.

Class vs. Instance Attributes

Both JavaScript and Python distinguish between class-level and instance-level attributes, but they do so differently:

JavaScript Static Properties and Methods

class MathUtils {
  // Static property (class-level)
  static PI = 3.14159;
  
  // Instance property (initialized in constructor)
  constructor(value) {
    this.value = value;
  }
  
  // Instance method
  square() {
    return this.value * this.value;
  }
  
  // Static method (class-level)
  static sum(a, b) {
    return a + b;
  }
}

Python Class and Instance Attributes

class MathUtils:
    # Class attribute (shared by all instances)
    PI = 3.14159
    
    def __init__(self, value):
        # Instance attribute (unique to each instance)
        self.value = value
    
    # Instance method
    def square(self):
        return self.value * self.value
    
    # Static method
    @staticmethod
    def sum(a, b):
        return a + b

Using these attributes and methods:

JavaScript

// Accessing static property and method
console.log(MathUtils.PI);  // 3.14159
console.log(MathUtils.sum(5, 3));  // 8

// Using instance
const math = new MathUtils(4);
console.log(math.value);  // 4
console.log(math.square());  // 16

// Static members aren't available on instances
console.log(math.PI);  // undefined
// And instance members aren't available on the class
console.log(MathUtils.value);  // undefined

Python

# Accessing class attribute and static method
print(MathUtils.PI)  # 3.14159
print(MathUtils.sum(5, 3))  # 8

# Using instance
math = MathUtils(4)
print(math.value)  # 4
print(math.square())  # 16

# Here's a key difference: class attributes ARE accessible from instances
print(math.PI)  # 3.14159
# But instance attributes aren't available on the class
print(MathUtils.value)  # AttributeError

A critical difference lies in how Python handles class attributes:

Python Class Attribute Behavior (Beware!)

class Counter:
    count = 0  # Class attribute shared by all instances
    
    def __init__(self, name):
        self.name = name  # Instance attribute
    
    def increment(self):
        self.count += 1  # CAUTION: This creates an instance attribute!
        return self.count

# Create counters
c1 = Counter("Counter 1")
c2 = Counter("Counter 2")

print(Counter.count)  # 0
print(c1.count)       # 0
print(c2.count)       # 0

# Increment c1's count
c1.increment()
print(c1.count)       # 1
print(c2.count)       # 0 (unchanged!)
print(Counter.count)  # 0 (unchanged!)

# Why? Because c1.count += 1 created a new instance attribute that shadows the class attribute!

# Correct way to modify class attribute:
Counter.count += 10
print(Counter.count)  # 10
print(c2.count)       # 10
print(c1.count)       # 1 (still has its own instance attribute)

Class attribute shadowing: One of the most confusing aspects of Python for JavaScript developers is this shadowing behavior. When you access instance.class_attribute, Python first checks if the instance has that attribute. If not, it looks for a class attribute with that name. But if you assign to instance.class_attribute, it always creates or updates an instance attribute, which then shadows the class attribute for that specific instance.

Inheritance

Inheritance syntax is similar in both languages, but with some important implementation differences:

JavaScript Inheritance

class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    return `${this.name} makes a noise.`;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);  // Call parent constructor
    this.breed = breed;
  }
  
  speak() {
    return `${this.name} barks!`;  // Override method
  }
  
  fetch() {
    return `${this.name} fetches the ball.`;  // New method
  }
}

Python Inheritance

class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return f"{self.name} makes a noise."

class Dog(Animal):  # Parentheses instead of 'extends'
    def __init__(self, name, breed):
        super().__init__(name)  # Call parent constructor
        self.breed = breed
    
    def speak(self):
        return f"{self.name} barks!"  # Override method
    
    def fetch(self):
        return f"{self.name} fetches the ball."  # New method

Using these classes:

JavaScript

const animal = new Animal("Animal");
const dog = new Dog("Buddy", "Golden Retriever");

console.log(animal.speak());  // "Animal makes a noise."
console.log(dog.speak());     // "Buddy barks!"
console.log(dog.fetch());     // "Buddy fetches the ball."

// Checking inheritance
console.log(dog instanceof Animal);  // true
console.log(dog instanceof Dog);     // true

Python

animal = Animal("Animal")
dog = Dog("Buddy", "Golden Retriever")

print(animal.speak())  # "Animal makes a noise."
print(dog.speak())     # "Buddy barks!"
print(dog.fetch())     # "Buddy fetches the ball."

# Checking inheritance
print(isinstance(dog, Animal))  # True
print(isinstance(dog, Dog))     # True
print(issubclass(Dog, Animal))  # True

Important differences in inheritance:

Multiple Inheritance in Python

Unlike JavaScript, Python supports inheriting from multiple parent classes:

class Swimmer:
    def swim(self):
        return "Swimming"

class Flyer:
    def fly(self):
        return "Flying"

class Duck(Swimmer, Flyer):  # Multiple inheritance
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        return "Quack!"

# Using the Duck class
duck = Duck("Donald")
print(duck.swim())  # "Swimming"
print(duck.fly())   # "Flying"
print(duck.speak()) # "Quack!"

Method Resolution Order (MRO): With multiple inheritance, Python needs to determine which parent class's method to call when methods are inherited from multiple parents. Python uses a deterministic algorithm called C3 linearization to establish the Method Resolution Order (MRO). You can view a class's MRO with ClassName.__mro__ or ClassName.mro().

Prototypes vs. Classes

JavaScript's class syntax is just syntactic sugar over its prototype-based inheritance. Understanding the underlying difference helps explain some behavioral differences:

JavaScript Class vs. Prototype Syntax

// Modern class syntax
class Person {
  constructor(name) {
    this.name = name;
  }
  
  greet() {
    return `Hello, my name is ${this.name}`;
  }
}

// Equivalent prototype syntax (pre-ES6)
function PersonProto(name) {
  this.name = name;
}

PersonProto.prototype.greet = function() {
  return `Hello, my name is ${this.name}`;
};

Key conceptual differences between JavaScript's prototype system and Python's class system:

Dynamic vs. Static Nature: JavaScript's prototype system is inherently more dynamic than Python's class system. In JavaScript, you can modify a class's prototype at runtime, and all existing instances will immediately have access to the new methods. In Python, adding methods to a class at runtime only affects future instances, not existing ones (unless you modify the class's __dict__ directly, which is generally discouraged).

Special Methods (Python Magic Methods)

Python has special methods (surrounded by double underscores) that let classes integrate with Python's built-in operators and functions. These are somewhat similar to JavaScript's Symbol-based methods but more extensive:

JavaScript Examples

class CustomArray {
  constructor(...items) {
    this.items = items;
  }
  
  // Iterable protocol
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.items.length) {
          return { value: this.items[index++], done: false };
        } else {
          return { done: true };
        }
      }
    };
  }
  
  // Custom string representation
  toString() {
    return `[${this.items.join(', ')}]`;
  }
  
  // Array-like access
  get length() {
    return this.items.length;
  }
}

const arr = new CustomArray(1, 2, 3);
console.log(arr.toString());  // "[1, 2, 3]"
console.log([...arr]);        // [1, 2, 3] (spreads using iterator)
console.log(arr.length);      // 3

Python Equivalent with Magic Methods

class CustomArray:
    def __init__(self, *items):
        self.items = items
    
    # Iterator protocol
    def __iter__(self):
        return iter(self.items)
    
    # String representation
    def __str__(self):
        return f"[{', '.join(str(item) for item in self.items)}]"
    
    # Developer representation 
    def __repr__(self):
        return f"CustomArray{self.items}"
    
    # Length function
    def __len__(self):
        return len(self.items)
    
    # Index access - get item
    def __getitem__(self, index):
        return self.items[index]

# Using the class
arr = CustomArray(1, 2, 3)
print(str(arr))      # "[1, 2, 3]"
print(list(arr))     # [1, 2, 3] (converts using iterator)
print(len(arr))      # 3
print(arr[1])        # 2 (uses __getitem__)

Python has many special methods for different operations:

Python Vector Class with Special Methods

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    # String representation
    def __str__(self):
        return f"({self.x}, {self.y})"
    
    # Addition (v1 + v2)
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    
    # Subtraction (v1 - v2)
    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)
    
    # Multiplication by scalar (v * 3)
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)
    
    # Reverse multiplication (3 * v)
    def __rmul__(self, scalar):
        return self.__mul__(scalar)
    
    # Equality (v1 == v2)
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y
    
    # Length (abs(v) or magnitude)
    def __abs__(self):
        return (self.x**2 + self.y**2)**0.5

# Using the Vector class
v1 = Vector(3, 4)
v2 = Vector(1, 2)

print(v1 + v2)        # (4, 6)
print(v1 - v2)        # (2, 2)
print(v1 * 2)         # (6, 8)
print(3 * v2)         # (3, 6)
print(v1 == Vector(3, 4))  # True
print(abs(v1))        # 5.0

Operator overloading: Python's special methods allow for operator overloading, making your classes work with Python's built-in operators and functions. This results in more readable, intuitive code. JavaScript has limited support for this through recent features like Symbol methods, but it's not as extensive or widely used.

Modules and Imports

Class organization differs between JavaScript and Python:

JavaScript Module Export

// person.js
class Person {
  constructor(name) {
    this.name = name;
  }
  
  greet() {
    return `Hello, I'm ${this.name}`;
  }
}

// Export the class
export default Person;
// Or named export
// export { Person };

JavaScript Module Import

// main.js
import Person from './person.js';
// Or named import
// import { Person } from './person.js';

const alice = new Person('Alice');
console.log(alice.greet());

Python Module Export

# person.py
class Person:
    def __init__(self, name):
        self.name = name
    
    def greet(self):
        return f"Hello, I'm {self.name}"

# No explicit export needed - all definitions are available

Python Module Import

# main.py
# Import the class
from person import Person
# Or import the entire module
# import person

alice = Person('Alice')
print(alice.greet())

Key differences in modules and imports:

Module system differences: JavaScript's module system was added later in the language's evolution, whereas Python's was there from the beginning. JavaScript requires explicit exports and has a richer syntax for managing imports/exports. Python's approach is simpler but less granular - typically one class per file is less common, and you often have multiple related classes in the same module.

Practical Example: Building a Todo App

Let's tie everything together with a practical example - a simple todo list application implemented in both languages:

JavaScript Todo App

// Define a Todo item class
class TodoItem {
  constructor(title, description = "") {
    this.title = title;
    this.description = description;
    this.completed = false;
    this.createdAt = new Date();
    this.completedAt = null;
  }
  
  complete() {
    this.completed = true;
    this.completedAt = new Date();
  }
  
  toString() {
    const status = this.completed ? "✓" : "□";
    return `[${status}] ${this.title}`;
  }
}

// Define a TodoList class
class TodoList {
  constructor(name) {
    this.name = name;
    this.items = [];
  }
  
  addItem(title, description = "") {
    const item = new TodoItem(title, description);
    this.items.push(item);
    return item;
  }
  
  completeItem(index) {
    if (index >= 0 && index < this.items.length) {
      this.items[index].complete();
      return true;
    }
    return false;
  }
  
  getCompletedItems() {
    return this.items.filter(item => item.completed);
  }
  
  getPendingItems() {
    return this.items.filter(item => !item.completed);
  }
  
  toString() {
    let result = `=== ${this.name} ===\n`;
    
    if (this.items.length === 0) {
      result += "No items\n";
    } else {
      this.items.forEach((item, i) => {
        result += `${i + 1}. ${item}\n`;
      });
    }
    
    return result;
  }
}

// Using the Todo classes
const todoList = new TodoList("My Tasks");
todoList.addItem("Learn JavaScript");
todoList.addItem("Learn Python", "Focus on OOP differences");
todoList.addItem("Build a project");

console.log(todoList.toString());

// Complete an item
todoList.completeItem(0);
console.log("\nAfter completing first item:");
console.log(todoList.toString());

// Check completed and pending
console.log("\nPending:", todoList.getPendingItems().length);
console.log("Completed:", todoList.getCompletedItems().length);

Python Todo App

import datetime

class TodoItem:
    def __init__(self, title, description=""):
        self.title = title
        self.description = description
        self.completed = False
        self.created_at = datetime.datetime.now()
        self.completed_at = None
    
    def complete(self):
        self.completed = True
        self.completed_at = datetime.datetime.now()
    
    def __str__(self):
        status = "✓" if self.completed else "□"
        return f"[{status}] {self.title}"

class TodoList:
    def __init__(self, name):
        self.name = name
        self.items = []
    
    def add_item(self, title, description=""):
        item = TodoItem(title, description)
        self.items.append(item)
        return item
    
    def complete_item(self, index):
        if 0 <= index < len(self.items):
            self.items[index].complete()
            return True
        return False
    
    def get_completed_items(self):
        return [item for item in self.items if item.completed]
    
    def get_pending_items(self):
        return [item for item in self.items if not item.completed]
    
    def __str__(self):
        result = f"=== {self.name} ===\n"
        
        if len(self.items) == 0:
            result += "No items\n"
        else:
            for i, item in enumerate(self.items):
                result += f"{i + 1}. {item}\n"
        
        return result

# Using the Todo classes
todo_list = TodoList("My Tasks")
todo_list.add_item("Learn JavaScript")
todo_list.add_item("Learn Python", "Focus on OOP differences")
todo_list.add_item("Build a project")

print(todo_list)

# Complete an item
todo_list.complete_item(0)
print("\nAfter completing first item:")
print(todo_list)

# Check completed and pending
print("\nPending:", len(todo_list.get_pending_items()))
print("Completed:", len(todo_list.get_completed_items()))

This example demonstrates:

Despite the syntactic differences, the structure and logic are remarkably similar. This highlights that once you understand the core OOP concepts, transitioning between languages is mostly about learning syntax differences and language-specific idioms.

Idiomatic differences: While the Todo app implementations look similar, there are subtle idiomatic differences. JavaScript tends to use more functional programming patterns (like filter), while Python often uses list comprehensions. Python uses special methods like __str__ where JavaScript uses named methods like toString. Learning these idiomatic differences is key to writing "Pythonic" code rather than "JavaScript in Python".

Conclusion: Embracing Python's OOP Style

As a JavaScript developer learning Python, embracing Python's approach to object-oriented programming will make your transition smoother. Here are some key takeaways:

Key Similarities

Key Differences to Remember

Becoming Pythonic: The goal isn't just to write working Python code, but to write "Pythonic" code—code that follows Python's idioms and conventions. This means embracing Python's design philosophy, including "Explicit is better than implicit" (hence self), "Simple is better than complex," and "Readability counts." As you continue learning Python, focus not just on making your code work, but on making it clear, readable, and idiomatic.

Your JavaScript background gives you a solid foundation in object-oriented concepts. By understanding the syntactic and behavioral differences outlined in this tutorial, you'll be well-equipped to write effective Python classes and leverage your existing OOP knowledge in this new language.

Practice Exercise

Take a small JavaScript class you've written before and convert it to Python, paying special attention to:

  1. Converting constructor to __init__ with explicit self parameter
  2. Changing camelCase names to snake_case
  3. Using appropriate Python special methods (__str__, __eq__, etc.)
  4. Implementing properties using the @property decorator instead of getters/setters
  5. Handling class vs. instance attributes correctly

This exercise will help reinforce the differences and similarities between JavaScript and Python OOP approaches.

Additional Resources