Python Full Stack Web Developer Course

Week 2: Thursday Afternoon - For JavaScript Developers: Python Functions vs JavaScript Functions

Bridging the Gap: From JavaScript to Python Functions

Welcome, JavaScript developers! This session is designed specifically for you to leverage your existing JavaScript knowledge as you learn Python. We'll explore the similarities and differences between functions in both languages, highlighting where your JS skills will help you and where Python's approach might surprise you.

This tutorial will be stored in your course files as: /week2_thursday_python_js_functions.html

Function Definition: The Basics

Syntax Comparison

Concept JavaScript Python
Basic Function Declaration
function greet(name) {
  return "Hello, " + name;
}
def greet(name):
    return "Hello, " + name
Function Expression
const greet = function(name) {
  return "Hello, " + name;
};
# Python doesn't have function
# expressions in the same way,
# but you can assign functions to
# variables
def say_hello(name):
    return "Hello, " + name

greet = say_hello
Arrow Functions
const greet = (name) => {
  return "Hello, " + name;
};

// Shorter version
const greet = name => "Hello, " + name;
# Python has lambda functions 
# (similar but more limited)
greet = lambda name: "Hello, " + name

# But they're limited to a single
# expression (no statements)

Key Differences

  • Indentation matters in Python: Unlike JavaScript's curly braces, Python uses indentation to define code blocks
  • No semicolons: Python statements don't end with semicolons
  • No function expressions: Python doesn't have function expressions in the same way JavaScript does
  • Limited lambdas: Python's lambda functions are much more restricted than JavaScript's arrow functions
  • No implicit returns: Python always requires an explicit return statement (except in lambdas)
  • No function hoisting: Python functions must be defined before they're called

Function Definition Analogy: Blueprints vs. Floor Plans

Think of JavaScript functions like architectural blueprints that can be moved around freely and referenced from anywhere in your plans. Python functions are more like floor plans that must be drawn before the builders can refer to them. Both define what a room looks like, but they have different rules about when and how they can be used.

Parameters and Arguments

Parameter Handling

Feature JavaScript Python
Default Parameters
function greet(name = "Guest") {
  return "Hello, " + name;
}
def greet(name="Guest"):
    return "Hello, " + name
Arguments Object / *args
function sum() {
  let total = 0;
  for (let i = 0; i < arguments.length; i++) {
    total += arguments[i];
  }
  return total;
}

sum(1, 2, 3); // Returns 6
def sum(*args):
    total = 0
    for num in args:
        total += num
    return total

sum(1, 2, 3)  # Returns 6
Named Parameters / **kwargs
function createPerson(options = {}) {
  const name = options.name || "Anonymous";
  const age = options.age || 0;
  return { name, age };
}

createPerson({ name: "John", age: 30 });
def create_person(**kwargs):
    name = kwargs.get("name", "Anonymous")
    age = kwargs.get("age", 0)
    return {"name": name, "age": age}

create_person(name="John", age=30)
Combining Parameter Types
function process(required, optional = "", ...rest) {
  console.log(required, optional, rest);
}

// Using object destructuring for named params
function settings({theme="dark", size="medium"} = {}) {
  console.log(theme, size);
}
def process(required, optional="", *args, **kwargs):
    print(required, optional, args, kwargs)
    
# Python has a strict parameter order:
# 1. Regular parameters
# 2. Default parameters
# 3. *args (variable positional)
# 4. **kwargs (variable keyword)

Key Insights

  • Python is more explicit: *args and **kwargs make the variable argument nature clear in the function signature
  • More concise named parameters: Python allows you to directly use named arguments without creating an options object
  • Strict parameter order: Python enforces a specific order for different parameter types
  • Default parameter evaluation: In Python, default parameters are evaluated only once at function definition time (this can cause unexpected behavior with mutable defaults)

Warning: Mutable Default Arguments in Python

One of the most common gotchas for JavaScript developers is Python's handling of mutable default parameters:

# This behaves unexpectedly in Python
def add_item(item, items=[]):  # The empty list is created ONCE when the function is defined
    items.append(item)
    return items

print(add_item("apple"))  # ["apple"]
print(add_item("banana"))  # ["apple", "banana"] - Surprise!

# The correct pattern is:
def add_item_fixed(item, items=None):
    if items is None:
        items = []  # Create a new list each time when no argument is provided
    items.append(item)
    return items

This happens because default arguments are evaluated only once at function definition time, not each time the function is called (unlike JavaScript).

Variable Scope and Closures

Scope Rules

Concept JavaScript Python
Function Scope
function outer() {
  const x = 10;
  
  function inner() {
    console.log(x);  // Can access x
  }
  
  inner();
}

outer();  // Outputs: 10
def outer():
    x = 10
    
    def inner():
        print(x)  # Can access x
    
    inner()

outer()  # Outputs: 10
Modifying Outer Variables
function counter() {
  let count = 0;
  
  return function() {
    count++;
    return count;
  };
}

const increment = counter();
console.log(increment());  // 1
console.log(increment());  // 2
def counter():
    count = 0
    
    def increment():
        nonlocal count  # Need this declaration
        count += 1
        return count
    
    return increment

increment = counter()
print(increment())  # 1
print(increment())  # 2
Global Variables
let globalVar = "I'm global";

function accessGlobal() {
  console.log(globalVar);  // Works fine
  
  // Modifying global variable
  globalVar = "Modified";
}

accessGlobal();
console.log(globalVar);  // "Modified"
global_var = "I'm global"

def access_global():
    print(global_var)  # Works fine
    
    # To modify global variable:
    global global_var
    global_var = "Modified"

access_global()
print(global_var)  # "Modified"

Key Differences in Scoping Rules

  • The nonlocal keyword: Python requires an explicit declaration to modify variables from outer scopes
  • The global keyword: Python requires explicit declaration to modify global variables inside functions
  • Block scope: JavaScript has block scope with let and const, but Python does not have block scope (if/while blocks don't create new scopes)
  • Variable shadowing: Both languages allow variable shadowing, but Python's scope rules can make it more surprising

Example: Python's Lack of Block Scope

# JavaScript with block scope
function blockScopeExample() {
  if (true) {
    let blockVar = "I'm in a block";
    const anotherVar = "Me too";
  }
  // console.log(blockVar);  // Error: blockVar is not defined
}

# Python has no block scope
def block_scope_example():
    if True:
        block_var = "I'm in a block"
        another_var = "Me too"
    
    print(block_var)      # Works fine! Prints "I'm in a block"
    print(another_var)    # Also works fine!

This can be surprising to JavaScript developers who are used to block-scoped variables with let and const.

Scope Analogy: Room Access Rules

Think of variable scope like access rules in a building:

  • JavaScript: Every code block (like an if statement) gets its own room with a door. Variables defined in a room stay in that room. Outer rooms can't see into inner rooms, but inner rooms can see out.
  • Python: Only functions create new rooms. If statements, loops, etc., are just areas within the same room. To modify variables from outer rooms, you need to explicitly ask for permission (using nonlocal or global).

Functions as First-Class Objects

Both JavaScript and Python treat functions as first-class objects, meaning you can:

First-Class Function Examples

Concept JavaScript Python
Passing Functions as Arguments
function applyOperation(a, b, operation) {
  return operation(a, b);
}

const add = (x, y) => x + y;
const multiply = (x, y) => x * y;

console.log(applyOperation(5, 3, add));      // 8
console.log(applyOperation(5, 3, multiply));  // 15
def apply_operation(a, b, operation):
    return operation(a, b)

def add(x, y):
    return x + y

def multiply(x, y):
    return x * y

print(apply_operation(5, 3, add))      # 8
print(apply_operation(5, 3, multiply))  # 15
Higher-Order Functions
function createMultiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15
def create_multiplier(factor):
    def multiplier(number):
        return number * factor
    return multiplier

double = create_multiplier(2)
triple = create_multiplier(3)

print(double(5))  # 10
print(triple(5))  # 15

Functional Programming Tools

Both languages provide built-in functions for common functional programming patterns, but with some differences:

Operation JavaScript Python
Map
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(x => x * 2);
// [2, 4, 6, 8]
numbers = [1, 2, 3, 4]
doubled = list(map(lambda x: x * 2, numbers))
# [2, 4, 6, 8]

# List comprehensions are often preferred:
doubled = [x * 2 for x in numbers]
# [2, 4, 6, 8]
Filter
const numbers = [1, 2, 3, 4, 5];
const evens = numbers.filter(x => x % 2 === 0);
// [2, 4]
numbers = [1, 2, 3, 4, 5]
evens = list(filter(lambda x: x % 2 == 0, numbers))
# [2, 4]

# List comprehensions are often preferred:
evens = [x for x in numbers if x % 2 == 0]
# [2, 4]
Reduce
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce(
  (acc, curr) => acc + curr, 0
);
// 10
from functools import reduce

numbers = [1, 2, 3, 4]
sum_result = reduce(
    lambda acc, curr: acc + curr, numbers, 0
)
# 10

# But Python often prefers built-ins:
sum_result = sum(numbers)  # 10

Key Insights

  • Both languages support functional programming, but with different syntax and conventions
  • Python often prefers comprehensions over functional-style map/filter operations
  • Python's lambda functions are more limited than JavaScript arrow functions (single expressions only)
  • Python moved reduce() to the functools module because sum(), min(), max(), etc., are considered more readable

Python Decorators vs. JavaScript Higher-Order Functions

Python decorators are a powerful feature that has some similarities to patterns used in JavaScript, but with more elegant syntax:

Function Enhancement

JavaScript Pattern Python Decorator
function logExecution(func) {
  return function(...args) {
    console.log(`Calling ${func.name}`);
    const result = func(...args);
    console.log(`Finished ${func.name}`);
    return result;
  };
}

// Usage:
function add(a, b) {
  return a + b;
}

const loggedAdd = logExecution(add);
loggedAdd(2, 3);  // Logs execution and returns 5
def log_execution(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Finished {func.__name__}")
        return result
    return wrapper

# Usage:
@log_execution
def add(a, b):
    return a + b

# The @ syntax automatically does:
# add = log_execution(add)

add(2, 3)  # Logs execution and returns 5

Python's decorator syntax (@decorator) provides a clean way to apply function transformations. While JavaScript doesn't have built-in decorator syntax (although it's been proposed), the underlying concept of wrapping functions to extend their behavior is similar.

Common Python Decorator Patterns

Timing Decorator
import time

def timing_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"{func.__name__} took {end_time - start_time:.4f} seconds to run")
        return result
    return wrapper

@timing_decorator
def slow_function():
    time.sleep(1)  # Simulate a slow operation
    return "Done!"

slow_function()  # Outputs: slow_function took 1.0005 seconds to run
Memoization Decorator
def memoize(func):
    cache = {}
    
    def wrapper(*args):
        if args in cache:
            return cache[args]
        result = func(*args)
        cache[args] = result
        return result
    
    return wrapper

@memoize
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(35))  # Fast calculation thanks to memoization
Applying Multiple Decorators
def bold(func):
    def wrapper(*args, **kwargs):
        return f"{func(*args, **kwargs)}"
    return wrapper

def italic(func):
    def wrapper(*args, **kwargs):
        return f"{func(*args, **kwargs)}"
    return wrapper

@bold
@italic
def greet(name):
    return f"Hello, {name}"

print(greet("World"))  # Hello, World
# Decorators are applied from bottom to top

Decorator Analogy: Gift Wrapping

Think of decorators like gift wrapping services:

  • Your function is the gift
  • The decorator is the wrapping service
  • You hand over your gift to the wrapper, and they return it with new features (wrapping, ribbon, tag)
  • The wrapped gift is still essentially the same gift, but with enhanced presentation or functionality
  • You can apply multiple wrappers (e.g., wrap, then add a bow, then add a tag)

The Python @decorator syntax is like a convenient drop-off counter where you can just tag your gift with instructions for how it should be wrapped.

Asynchronous Patterns

JavaScript and Python handle asynchronous programming quite differently:

Asynchronous Programming

JavaScript Python
// Callbacks (older style)
function fetchData(callback) {
  setTimeout(() => {
    callback("Data received");
  }, 1000);
}

fetchData(data => {
  console.log(data);  // After 1 second
});

// Promises (modern style)
function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("Data received");
    }, 1000);
  });
}

fetchData().then(data => {
  console.log(data);  // After 1 second
});

// Async/await (newest style)
async function getData() {
  const data = await fetchData();
  console.log(data);  // After 1 second
}

getData();
# Traditional approach with callbacks
def fetch_data(callback):
    import threading
    def task():
        import time
        time.sleep(1)
        callback("Data received")
    
    thread = threading.Thread(target=task)
    thread.start()

def handle_data(data):
    print(data)  # After 1 second

fetch_data(handle_data)

# Modern Python with asyncio (Python 3.5+)
import asyncio

async def fetch_data():
    await asyncio.sleep(1)
    return "Data received"

async def get_data():
    data = await fetch_data()
    print(data)  # After 1 second

# Run the coroutine
asyncio.run(get_data())  # Python 3.7+

Key Differences in Async Patterns

  • JavaScript is single-threaded with an event loop built into the language/runtime
  • Python has multiple approaches: threads, processes, and asyncio
  • JavaScript async/await works with any Promise-returning function
  • Python async/await works only with coroutines and awaitable objects
  • JavaScript's event loop is always running, while Python's asyncio event loop must be started explicitly
  • Python has the GIL (Global Interpreter Lock), which affects threading behavior

Python asyncio for JavaScript Developers

Python's asyncio might feel familiar if you've used async/await in JavaScript, but there are important differences:

# Python asyncio example (Python 3.7+)
import asyncio

async def fetch_user(user_id):
    await asyncio.sleep(1)  # Simulates API call
    return {"id": user_id, "name": f"User {user_id}"}

async def fetch_posts(user_id):
    await asyncio.sleep(0.5)  # Simulates API call
    return [{"id": i, "title": f"Post {i}"} for i in range(3)]

async def get_user_data(user_id):
    # Run tasks concurrently (like Promise.all)
    user, posts = await asyncio.gather(
        fetch_user(user_id),
        fetch_posts(user_id)
    )
    
    return {
        "user": user,
        "posts": posts
    }

# Run the async function
result = asyncio.run(get_user_data(123))
print(result)

In JavaScript, this pattern would look like:

// Equivalent JavaScript with async/await
async function fetchUser(userId) {
  await new Promise(resolve => setTimeout(resolve, 1000));
  return { id: userId, name: `User ${userId}` };
}

async function fetchPosts(userId) {
  await new Promise(resolve => setTimeout(resolve, 500));
  return Array.from({ length: 3 }, (_, i) => ({ 
    id: i, title: `Post ${i}` 
  }));
}

async function getUserData(userId) {
  // Run tasks concurrently (Promise.all)
  const [user, posts] = await Promise.all([
    fetchUser(userId),
    fetchPosts(userId)
  ]);
  
  return {
    user,
    posts
  };
}

// Call the async function
getUserData(123).then(result => console.log(result));

Coding Style and Conventions

Style Differences

Aspect JavaScript Python
Naming Conventions
  • camelCase for functions, variables
  • PascalCase for classes
  • UPPER_CASE for constants
  • snake_case for functions, variables
  • PascalCase for classes
  • UPPER_CASE for constants
String Formatting
// Template literals
const name = "John";
const greeting = `Hello, ${name}!`;

// String concatenation
const old = "Hello, " + name + "!";
# f-strings (Python 3.6+)
name = "John"
greeting = f"Hello, {name}!"

# str.format() method
greeting = "Hello, {}!".format(name)

# %-formatting (older style)
greeting = "Hello, %s!" % name
Documentation
/**
 * Calculates the sum of two numbers.
 * @param {number} a - First number
 * @param {number} b - Second number
 * @returns {number} The sum of a and b
 */
function add(a, b) {
  return a + b;
}
def add(a, b):
    """
    Calculates the sum of two numbers.
    
    Args:
        a: First number
        b: Second number
        
    Returns:
        The sum of a and b
    """
    return a + b

Style Guide Differences

JavaScript and Python have different style guides that reflect their communities' values:

  • Python: PEP 8 is the official style guide, emphasizing readability and consistency
  • JavaScript: Multiple popular style guides (Airbnb, Standard, Google), with more variation in accepted styles
  • Python: "There should be one-- and preferably only one --obvious way to do it"
  • JavaScript: Multiple ways to accomplish the same task are common

Tips for JavaScript Developers Writing Python

  • Use snake_case for functions and variables (not camelCase)
  • Follow indentation strictly (4 spaces is the Python standard)
  • Embrace Pythonic idioms like list comprehensions and context managers
  • Use docstrings for documentation (triple-quoted strings)
  • Remember that Python has different truthiness rules (empty lists/dicts are falsy)
  • Use f-strings for string formatting (they're similar to JS template literals)
  • Avoid one-liners and overly clever code; Python values readability

Practical Examples: JavaScript to Python Translation

Example 1: API Request Handler

JavaScript Version:
// API request handler in Express.js
const express = require('express');
const app = express();

app.get('/api/users/:id', async (req, res) => {
  try {
    const userId = parseInt(req.params.id);
    
    if (isNaN(userId)) {
      return res.status(400).json({ error: 'Invalid user ID' });
    }
    
    const user = await getUserFromDatabase(userId);
    
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    
    return res.json(user);
  } catch (error) {
    console.error('Error fetching user:', error);
    return res.status(500).json({ error: 'Internal server error' });
  }
});
Python Version (Flask):
from flask import Flask, jsonify, request
app = Flask(__name__)

@app.route('/api/users/')
def get_user(user_id):
    try:
        user_id = int(user_id)
    except ValueError:
        return jsonify({"error": "Invalid user ID"}), 400
    
    try:
        user = get_user_from_database(user_id)
        
        if not user:
            return jsonify({"error": "User not found"}), 404
        
        return jsonify(user)
    except Exception as e:
        app.logger.error(f"Error fetching user: {e}")
        return jsonify({"error": "Internal server error"}), 500

Example 2: Data Processing Function

JavaScript Version:
// Process user data
function processUserData(users) {
  if (!Array.isArray(users) || users.length === 0) {
    return { error: 'Invalid user data' };
  }
  
  // Filter active users
  const activeUsers = users.filter(user => user.status === 'active');
  
  // Calculate average age
  const totalAge = activeUsers.reduce((sum, user) => sum + user.age, 0);
  const averageAge = activeUsers.length > 0 ? totalAge / activeUsers.length : 0;
  
  // Group users by country
  const usersByCountry = activeUsers.reduce((result, user) => {
    const country = user.country || 'Unknown';
    if (!result[country]) {
      result[country] = [];
    }
    result[country].push(user);
    return result;
  }, {});
  
  return {
    totalUsers: users.length,
    activeUsers: activeUsers.length,
    averageAge: averageAge.toFixed(1),
    usersByCountry
  };
}
Python Version:
def process_user_data(users):
    if not isinstance(users, list) or not users:
        return {"error": "Invalid user data"}
    
    # Filter active users
    active_users = [user for user in users if user.get('status') == 'active']
    
    # Calculate average age
    total_age = sum(user.get('age', 0) for user in active_users)
    average_age = total_age / len(active_users) if active_users else 0
    
    # Group users by country
    users_by_country = {}
    for user in active_users:
        country = user.get('country', 'Unknown')
        if country not in users_by_country:
            users_by_country[country] = []
        users_by_country[country].append(user)
    
    return {
        "total_users": len(users),
        "active_users": len(active_users),
        "average_age": f"{average_age:.1f}",
        "users_by_country": users_by_country
    }
More Pythonic Version (using collections):
from collections import defaultdict

def process_user_data(users):
    if not isinstance(users, list) or not users:
        return {"error": "Invalid user data"}
    
    # Filter active users with list comprehension
    active_users = [user for user in users if user.get('status') == 'active']
    
    # Calculate average age with generator expression
    ages = (user.get('age', 0) for user in active_users)
    average_age = sum(ages) / len(active_users) if active_users else 0
    
    # Group users by country with defaultdict
    users_by_country = defaultdict(list)
    for user in active_users:
        country = user.get('country', 'Unknown')
        users_by_country[country].append(user)
    
    return {
        "total_users": len(users),
        "active_users": len(active_users),
        "average_age": f"{average_age:.1f}",
        "users_by_country": dict(users_by_country)  # Convert back to regular dict
    }

Practice Exercises

Exercise 1: Convert this JavaScript function to Python

// JavaScript version
function calculateDiscount(price, customerType) {
  if (typeof price !== 'number' || price <= 0) {
    throw new Error('Price must be a positive number');
  }
  
  let discountRate = 0;
  
  switch (customerType) {
    case 'regular':
      discountRate = 0.05;
      break;
    case 'premium':
      discountRate = 0.1;
      break;
    case 'vip':
      discountRate = 0.15;
      break;
    default:
      discountRate = 0;
  }
  
  // Additional discount for high-value purchases
  if (price > 1000) {
    discountRate += 0.05;
  }
  
  const discount = price * discountRate;
  return {
    originalPrice: price,
    discountRate: discountRate,
    discount: discount,
    finalPrice: price - discount
  };
}
Solution
def calculate_discount(price, customer_type):
    """
    Calculate discount based on price and customer type.
    
    Args:
        price: The original price (must be positive number)
        customer_type: Customer type (regular, premium, vip)
        
    Returns:
        Dictionary with price details and discount
        
    Raises:
        ValueError: If price is not a positive number
    """
    if not isinstance(price, (int, float)) or price <= 0:
        raise ValueError("Price must be a positive number")
    
    # Determine base discount rate by customer type
    discount_rates = {
        'regular': 0.05,
        'premium': 0.1,
        'vip': 0.15
    }
    discount_rate = discount_rates.get(customer_type, 0)
    
    # Additional discount for high-value purchases
    if price > 1000:
        discount_rate += 0.05
    
    discount = price * discount_rate
    
    return {
        "original_price": price,
        "discount_rate": discount_rate,
        "discount": discount,
        "final_price": price - discount
    }

Exercise 2: Implement a Python decorator equivalent to this JavaScript wrapper

// JavaScript function wrapper
function validateArgs(validator) {
  return function(func) {
    return function(...args) {
      const valid = validator(...args);
      if (!valid) {
        throw new Error('Invalid arguments');
      }
      return func(...args);
    };
  };
}

// Usage
const requirePositiveNumbers = (...args) => 
  args.every(arg => typeof arg === 'number' && arg > 0);

const divide = (a, b) => a / b;

const safeDivide = validateArgs(requirePositiveNumbers)(divide);

// This works
console.log(safeDivide(10, 2));  // 5

// This throws an error
try {
  console.log(safeDivide(-10, 2));
} catch (e) {
  console.error(e.message);  // "Invalid arguments"
}
Solution
def validate_args(validator):
    """
    Create a decorator that validates function arguments.
    
    Args:
        validator: A function that takes the same args as the wrapped function
                  and returns True if they're valid, False otherwise
    
    Returns:
        A decorator function
    """
    def decorator(func):
        def wrapper(*args, **kwargs):
            if not validator(*args, **kwargs):
                raise ValueError("Invalid arguments")
            return func(*args, **kwargs)
        return wrapper
    return decorator

# Usage
def require_positive_numbers(*args, **kwargs):
    """Check if all positional arguments are positive numbers"""
    return all(isinstance(arg, (int, float)) and arg > 0 for arg in args)

def divide(a, b):
    return a / b

# Apply the decorator
safe_divide = validate_args(require_positive_numbers)(divide)

# Alternative syntax with decorator
@validate_args(require_positive_numbers)
def safe_divide_alt(a, b):
    return a / b

# This works
print(safe_divide(10, 2))  # 5

# This raises an exception
try:
    print(safe_divide(-10, 2))
except ValueError as e:
    print(e)  # "Invalid arguments"

Exercise 3: Convert this JavaScript class to Python

// JavaScript class
class TaskManager {
  constructor() {
    this.tasks = [];
    this.completedTasks = [];
  }
  
  addTask(title, priority = 'medium') {
    const task = {
      id: Date.now(),
      title,
      priority,
      completed: false,
      createdAt: new Date()
    };
    
    this.tasks.push(task);
    return task.id;
  }
  
  completeTask(taskId) {
    const taskIndex = this.tasks.findIndex(task => task.id === taskId);
    
    if (taskIndex === -1) {
      throw new Error(`Task with ID ${taskId} not found`);
    }
    
    const task = this.tasks[taskIndex];
    task.completed = true;
    task.completedAt = new Date();
    
    this.completedTasks.push(task);
    this.tasks.splice(taskIndex, 1);
    
    return task;
  }
  
  getTasksByPriority(priority) {
    return this.tasks.filter(task => task.priority === priority);
  }
  
  getStats() {
    return {
      pending: this.tasks.length,
      completed: this.completedTasks.length,
      total: this.tasks.length + this.completedTasks.length
    };
  }
}
Solution
import time
from datetime import datetime

class TaskManager:
    """A class to manage tasks with priorities and completion status."""
    
    def __init__(self):
        """Initialize an empty task manager."""
        self.tasks = []
        self.completed_tasks = []
    
    def add_task(self, title, priority='medium'):
        """
        Add a new task to the manager.
        
        Args:
            title: The task title
            priority: The task priority (default: 'medium')
            
        Returns:
            The ID of the new task
        """
        task = {
            'id': int(time.time() * 1000),  # Similar to Date.now()
            'title': title,
            'priority': priority,
            'completed': False,
            'created_at': datetime.now()
        }
        
        self.tasks.append(task)
        return task['id']
    
    def complete_task(self, task_id):
        """
        Mark a task as completed and move it to completed_tasks.
        
        Args:
            task_id: The ID of the task to complete
            
        Returns:
            The completed task
            
        Raises:
            ValueError: If the task is not found
        """
        # Find the task by ID
        task_index = None
        for i, task in enumerate(self.tasks):
            if task['id'] == task_id:
                task_index = i
                break
        
        if task_index is None:
            raise ValueError(f"Task with ID {task_id} not found")
        
        # Update and move the task
        task = self.tasks[task_index]
        task['completed'] = True
        task['completed_at'] = datetime.now()
        
        self.completed_tasks.append(task)
        self.tasks.pop(task_index)  # Remove from tasks list
        
        return task
    
    def get_tasks_by_priority(self, priority):
        """
        Get all pending tasks with the specified priority.
        
        Args:
            priority: The priority to filter by
            
        Returns:
            List of tasks with the specified priority
        """
        return [task for task in self.tasks if task['priority'] == priority]
    
    def get_stats(self):
        """
        Get statistics about tasks.
        
        Returns:
            Dictionary with pending, completed, and total counts
        """
        return {
            'pending': len(self.tasks),
            'completed': len(self.completed_tasks),
            'total': len(self.tasks) + len(self.completed_tasks)
        }

Assignment: Data Processing Utility

Create a Python module with functions for processing and analyzing data from a JSON file. Your implementation should showcase Python's function features in comparison to JavaScript.

Requirements:

  1. Create a module named data_processor.py
  2. Implement functions to:
    • Load data from a JSON file with error handling
    • Filter data based on multiple criteria
    • Transform data (e.g., calculate new fields, format values)
    • Group and aggregate data
    • Export processed data to a new file
  3. Use Python features that differ from JavaScript:
    • List comprehensions instead of array methods
    • Default parameters
    • *args and **kwargs
    • Type hints (Python 3.5+)
    • At least one decorator
    • Context managers for file operations
  4. Include docstrings and comments explaining your code
  5. Create a simple demonstration script that uses your module

Sample JSON data structure (create a file named sample_data.json):

{
  "products": [
    {
      "id": "p1",
      "name": "Laptop",
      "category": "Electronics",
      "price": 1200,
      "stock": 45,
      "ratings": [4, 5, 3, 5, 4]
    },
    {
      "id": "p2",
      "name": "Smartphone",
      "category": "Electronics",
      "price": 800,
      "stock": 20,
      "ratings": [5, 4, 4, 3, 5, 5]
    },
    // More products...
  ],
  "sales": [
    {
      "id": "s1",
      "product_id": "p1",
      "date": "2023-05-15",
      "quantity": 2,
      "customer_id": "c1"
    },
    // More sales...
  ],
  "customers": [
    {
      "id": "c1",
      "name": "John Doe",
      "email": "john@example.com",
      "membership": "premium"
    },
    // More customers...
  ]
}

Tips:

Bonus: Implement a simple CLI interface using argparse to run different data processing operations.

Submission location: /week2_thursday_data_processor.py and /week2_thursday_demo.py

Conclusion

As a JavaScript developer learning Python, you've already mastered many of the fundamental programming concepts. The key to becoming proficient in Python isn't just learning new syntax, but understanding the language's philosophy and idioms:

Key Takeaways

  • Python and JavaScript share many concepts: Both are high-level, dynamically typed languages with first-class functions
  • Embrace Python's philosophy: "There should be one—and preferably only one—obvious way to do it" vs. JavaScript's multiple approaches
  • Leverage Pythonic patterns: List comprehensions, context managers, and decorators are more elegant than their JavaScript equivalents
  • Mind the scope differences: Python's scoping rules are different, especially regarding block scope and variable modification
  • Watch for subtle gotchas: Like mutable default arguments and different equality behavior
  • Follow Python style conventions: snake_case, 4-space indentation, and PEP 8 guidelines

Remember, the goal isn't to write JavaScript in Python, but to learn to think in Python. By understanding the similarities and differences between the two languages, you can leverage your existing knowledge while adapting to Python's unique approach and idioms.

"Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated."
— The Zen of Python