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 Expression |
|
|
| Arrow Functions |
|
|
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 |
|
|
| Arguments Object / *args |
|
|
| Named Parameters / **kwargs |
|
|
| Combining Parameter Types |
|
|
Key Insights
- Python is more explicit:
*argsand**kwargsmake 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 |
|
|
| Modifying Outer Variables |
|
|
| Global Variables |
|
|
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
letandconst, 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
nonlocalorglobal).
Functions as First-Class Objects
Both JavaScript and Python treat functions as first-class objects, meaning you can:
- Assign functions to variables
- Pass functions as arguments to other functions
- Return functions from other functions
- Store functions in data structures
First-Class Function Examples
| Concept | JavaScript | Python |
|---|---|---|
| Passing Functions as Arguments |
|
|
| Higher-Order Functions |
|
|
Functional Programming Tools
Both languages provide built-in functions for common functional programming patterns, but with some differences:
| Operation | JavaScript | Python |
|---|---|---|
| Map |
|
|
| Filter |
|
|
| Reduce |
|
|
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 |
|---|---|
|
|
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 |
|---|---|
|
|
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 |
|
|
| String Formatting |
|
|
| Documentation |
|
|
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:
- Create a module named
data_processor.py - 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
- 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
- Include docstrings and comments explaining your code
- 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:
- Focus on writing Pythonic code, not just converting JavaScript patterns
- Use Python's standard library where appropriate
- Consider performance and error handling
- Add clear docstrings explaining how each function differs from a JavaScript approach
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