Skip to main content

🎛️ Lesson 4.2: Parameters, Arguments, and Return Values

Functions become truly powerful when they're flexible. Default values, keyword arguments, and variable-length parameter lists let you write functions that adapt to different situations.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Use default parameter values in all three languages
  • Pass arguments by position and by name (keyword arguments)
  • Accept a variable number of arguments (*args, rest/spread, params)
  • Understand method overloading in C# (and why Python/JS don't need it)
  • Use early returns for cleaner function logic

Estimated Time: 45 minutes

Project: A flexible order pricing function with optional tax, discount, and tip

📑 In This Lesson

Default Parameters

Default parameters let you give a parameter a fallback value. If the caller doesn't provide that argument, the default kicks in. This makes functions more flexible without requiring every argument every time.

# Default values go in the function definition
def greet(name, greeting="Hello"):
    print(f"{greeting}, {name}!")

greet("Alice")               # Hello, Alice!  (uses default)
greet("Bob", "Good morning") # Good morning, Bob!  (overrides default)

# Multiple defaults
def create_user(name, role="viewer", active=True):
    print(f"User: {name}, Role: {role}, Active: {active}")

create_user("Alice")                        # viewer, True
create_user("Bob", "admin")                 # admin, True
create_user("Charlie", "editor", False)     # editor, False
// Default values with = in the parameter list
function greet(name, greeting = "Hello") {
    console.log(`${greeting}, ${name}!`);
}

greet("Alice");                // Hello, Alice!
greet("Bob", "Good morning"); // Good morning, Bob!

// Multiple defaults
function createUser(name, role = "viewer", active = true) {
    console.log(`User: ${name}, Role: ${role}, Active: ${active}`);
}

createUser("Alice");                        // viewer, true
createUser("Bob", "admin");                 // admin, true
createUser("Charlie", "editor", false);     // editor, false
// Default values with = (called "optional parameters")
static void Greet(string name, string greeting = "Hello")
{
    Console.WriteLine($"{greeting}, {name}!");
}

Greet("Alice");                // Hello, Alice!
Greet("Bob", "Good morning"); // Good morning, Bob!

// Multiple defaults
static void CreateUser(string name, string role = "viewer", bool active = true)
{
    Console.WriteLine($"User: {name}, Role: {role}, Active: {active}");
}

CreateUser("Alice");                        // viewer, True
CreateUser("Bob", "admin");                 // admin, True
CreateUser("Charlie", "editor", false);     // editor, False

⚠️ Rule: Defaults Must Come Last

In all three languages, parameters with defaults must come after parameters without defaults. This makes sense — otherwise the language wouldn't know which arguments go where.

# ❌ Wrong — default before non-default
def greet(greeting="Hello", name):  # SyntaxError!

# ✅ Right — defaults at the end
def greet(name, greeting="Hello"):  # Works!
🎓 Instructor Note: Delivery Guidance

Default parameters are one of the most immediately useful features students will learn. Show practical examples: a logging function with a default severity level, an API function with a default timeout, or a game function with default difficulty. The "defaults must come last" rule is intuitive once you think about it — walk through why it would be ambiguous otherwise.

Keyword (Named) Arguments

Instead of relying on argument position, you can specify which parameter each argument goes to by name. This is especially useful when a function has many parameters with defaults.

def create_user(name, role="viewer", active=True, email=None):
    print(f"Name: {name}")
    print(f"Role: {role}")
    print(f"Active: {active}")
    print(f"Email: {email or 'Not provided'}")
    print()

# Positional: must be in order
create_user("Alice", "admin", True, "alice@example.com")

# Keyword: any order, self-documenting
create_user("Bob", email="bob@example.com", role="editor")

# Mix positional and keyword (positional must come first)
create_user("Charlie", "admin", email="charlie@example.com")

# Skip middle defaults easily:
create_user("Diana", active=False)  # role stays "viewer"
// JavaScript doesn't have built-in keyword args
// Instead, use an options OBJECT (very common pattern)

function createUser({ name, role = "viewer", active = true, email = null }) {
    console.log(`Name: ${name}`);
    console.log(`Role: ${role}`);
    console.log(`Active: ${active}`);
    console.log(`Email: ${email || "Not provided"}`);
    console.log();
}

// Pass an object — order doesn't matter
createUser({ name: "Alice", role: "admin", email: "alice@example.com" });
createUser({ name: "Bob", email: "bob@example.com", role: "editor" });

// Skip any defaults you don't need
createUser({ name: "Charlie", active: false });

// This "options object" pattern is everywhere in JavaScript:
// fetch(url, { method: "POST", headers: {...} })
// addEventListener("click", handler, { once: true })
// C# supports named arguments with the name: value syntax
static void CreateUser(string name, string role = "viewer",
    bool active = true, string email = null)
{
    Console.WriteLine($"Name: {name}");
    Console.WriteLine($"Role: {role}");
    Console.WriteLine($"Active: {active}");
    Console.WriteLine($"Email: {email ?? "Not provided"}");
    Console.WriteLine();
}

// Positional
CreateUser("Alice", "admin", true, "alice@example.com");

// Named: any order (after positional args)
CreateUser("Bob", email: "bob@example.com", role: "editor");

// Skip middle defaults
CreateUser("Charlie", active: false);

// C# named args look like Python keyword args!
// The key difference: C# also has method overloading (see below)

💡 Three Approaches to the Same Problem

Python and C# have built-in keyword arguments. JavaScript doesn't — instead, it uses the "options object" pattern (passing a single object with named properties). All three achieve the same goal: readable, flexible function calls where you don't have to remember argument order.

Variable-Length Arguments

Sometimes you don't know how many arguments a function will receive. All three languages have a way to accept any number of arguments.

# *args collects extra positional arguments into a tuple
def add_all(*numbers):
    total = sum(numbers)
    print(f"Sum of {numbers} = {total}")
    return total

add_all(1, 2, 3)          # Sum of (1, 2, 3) = 6
add_all(10, 20, 30, 40)   # Sum of (10, 20, 30, 40) = 100

# **kwargs collects extra keyword arguments into a dictionary
def print_info(name, **details):
    print(f"Name: {name}")
    for key, value in details.items():
        print(f"  {key}: {value}")

print_info("Alice", age=30, city="NYC", role="developer")
# Name: Alice
#   age: 30
#   city: NYC
#   role: developer

# You can use both together:
def flexible(required, *args, **kwargs):
    print(f"Required: {required}")
    print(f"Extra args: {args}")
    print(f"Extra kwargs: {kwargs}")
// Rest parameter (...) collects remaining args into an array
function addAll(...numbers) {
    let total = numbers.reduce((sum, n) => sum + n, 0);
    console.log(`Sum of [${numbers}] = ${total}`);
    return total;
}

addAll(1, 2, 3);          // Sum of [1,2,3] = 6
addAll(10, 20, 30, 40);   // Sum of [10,20,30,40] = 100

// Rest must be the last parameter
function greetAll(greeting, ...names) {
    for (let name of names) {
        console.log(`${greeting}, ${name}!`);
    }
}
greetAll("Hello", "Alice", "Bob", "Charlie");

// Spread operator (...) does the opposite — unpacks an array
let nums = [1, 2, 3, 4, 5];
console.log(Math.max(...nums));  // 5 (spreads array into arguments)
// params keyword collects extra arguments into an array
static int AddAll(params int[] numbers)
{
    int total = numbers.Sum();
    Console.WriteLine($"Sum of [{string.Join(", ", numbers)}] = {total}");
    return total;
}

AddAll(1, 2, 3);          // Sum of [1, 2, 3] = 6
AddAll(10, 20, 30, 40);   // Sum of [10, 20, 30, 40] = 100

// params must be the last parameter and there can only be one
static void GreetAll(string greeting, params string[] names)
{
    foreach (string name in names)
    {
        Console.WriteLine($"{greeting}, {name}!");
    }
}
GreetAll("Hello", "Alice", "Bob", "Charlie");

// You can also pass an array directly:
string[] people = { "Diana", "Eve" };
GreetAll("Hi", people);

Quick Reference

Feature 🐍 Python ⚡ JavaScript 🔷 C#
Collect extra args *args (tuple) ...args (array) params type[]
Collect named args **kwargs (dict) Use options object Not built-in
Unpack/spread *list, **dict ...array Pass array directly

Method Overloading (C#)

C# has a feature Python and JavaScript lack: method overloading. You can define multiple methods with the same name but different parameter lists. The compiler picks the right one based on the arguments you pass.

# Python doesn't have overloading — use default params instead
def calculate_area(width, height=None):
    if height is None:
        # Treat as a square
        return width * width
    return width * height

print(calculate_area(5))       # 25 (square)
print(calculate_area(5, 3))    # 15 (rectangle)

# Or use *args for more flexibility:
def greet(*args):
    if len(args) == 1:
        print(f"Hello, {args[0]}!")
    elif len(args) == 2:
        print(f"{args[1]}, {args[0]}!")

greet("Alice")               # Hello, Alice!
greet("Alice", "Good morning")  # Good morning, Alice!
// JavaScript doesn't have overloading — use defaults or type checks
function calculateArea(width, height = null) {
    if (height === null) {
        return width * width;  // square
    }
    return width * height;     // rectangle
}

console.log(calculateArea(5));      // 25 (square)
console.log(calculateArea(5, 3));   // 15 (rectangle)

// Or check argument types:
function format(value) {
    if (typeof value === "number") {
        return value.toFixed(2);
    } else if (typeof value === "string") {
        return value.toUpperCase();
    }
    return String(value);
}

console.log(format(3.14159));   // "3.14"
console.log(format("hello"));  // "HELLO"
// C# method overloading — same name, different parameters
static double CalculateArea(double side)
{
    // Square: one parameter
    return side * side;
}

static double CalculateArea(double width, double height)
{
    // Rectangle: two parameters
    return width * height;
}

static double CalculateArea(double radius, bool isCircle)
{
    // Circle: radius + flag
    return Math.PI * radius * radius;
}

// The compiler picks the right version automatically:
Console.WriteLine(CalculateArea(5));           // 25 (square)
Console.WriteLine(CalculateArea(5, 3));        // 15 (rectangle)
Console.WriteLine(CalculateArea(5, true));     // 78.54 (circle)

// Overloading also works with different types:
static string Format(int value) => value.ToString("N0");
static string Format(double value) => value.ToString("F2");
static string Format(string value) => value.ToUpper();

Console.WriteLine(Format(1000));      // "1,000"
Console.WriteLine(Format(3.14159));   // "3.14"
Console.WriteLine(Format("hello"));   // "HELLO"

💡 Why C# Has Overloading and Python/JS Don't

C# is statically typed — the compiler knows parameter types at compile time, so it can pick the right overload. Python and JavaScript are dynamically typed — they figure out types at runtime, so they use default parameters, type checks, or *args patterns instead. Different approach, same goal: functions that handle different inputs gracefully.

Early Returns

Instead of nesting everything in if/else blocks, you can use return to exit a function early when certain conditions are met. This makes functions flatter and easier to read.

# ❌ Deeply nested — hard to follow
def process_order_nested(item, quantity, price):
    if item:
        if quantity > 0:
            if price > 0:
                total = quantity * price
                return f"Order: {quantity}x {item} = ${total:.2f}"
            else:
                return "Error: Invalid price"
        else:
            return "Error: Invalid quantity"
    else:
        return "Error: No item specified"

# ✅ Early returns — flat and clean
def process_order(item, quantity, price):
    if not item:
        return "Error: No item specified"
    if quantity <= 0:
        return "Error: Invalid quantity"
    if price <= 0:
        return "Error: Invalid price"

    total = quantity * price
    return f"Order: {quantity}x {item} = ${total:.2f}"

print(process_order("Widget", 3, 9.99))   # Order: 3x Widget = $29.97
print(process_order("", 3, 9.99))          # Error: No item specified
print(process_order("Widget", -1, 9.99))   # Error: Invalid quantity
// ✅ Early returns — guard clauses at the top
function processOrder(item, quantity, price) {
    if (!item) return "Error: No item specified";
    if (quantity <= 0) return "Error: Invalid quantity";
    if (price <= 0) return "Error: Invalid price";

    let total = quantity * price;
    return `Order: ${quantity}x ${item} = $${total.toFixed(2)}`;
}

console.log(processOrder("Widget", 3, 9.99));
console.log(processOrder("", 3, 9.99));
console.log(processOrder("Widget", -1, 9.99));
// ✅ Early returns — guard clauses at the top
static string ProcessOrder(string item, int quantity, decimal price)
{
    if (string.IsNullOrEmpty(item)) return "Error: No item specified";
    if (quantity <= 0) return "Error: Invalid quantity";
    if (price <= 0) return "Error: Invalid price";

    decimal total = quantity * price;
    return $"Order: {quantity}x {item} = {total:C}";
}

Console.WriteLine(ProcessOrder("Widget", 3, 9.99m));
Console.WriteLine(ProcessOrder("", 3, 9.99m));
Console.WriteLine(ProcessOrder("Widget", -1, 9.99m));

✅ The Guard Clause Pattern

Check for invalid inputs at the top of your function and return immediately. This way, the "happy path" (the normal case) isn't buried inside nested conditions. Most professional codebases follow this pattern — it's one of the simplest ways to improve code readability.

Pass by Value vs. Pass by Reference

When you pass a variable to a function, does the function get a copy of the value or a reference to the original? The answer depends on the type of data.

# Numbers, strings, booleans: changes inside don't affect outside
def try_to_change(x):
    x = 999
    print(f"Inside: x = {x}")

num = 42
try_to_change(num)
print(f"Outside: num = {num}")  # Still 42!

# Lists, dicts: changes inside DO affect outside
def add_item(items):
    items.append("new item")
    print(f"Inside: {items}")

my_list = ["a", "b"]
add_item(my_list)
print(f"Outside: {my_list}")  # ['a', 'b', 'new item'] — changed!

# Why? Python passes a reference to the object.
# Numbers are immutable (x = 999 creates a NEW number).
# Lists are mutable (append modifies the SAME list).
// Primitives (numbers, strings, booleans): copied
function tryToChange(x) {
    x = 999;
    console.log(`Inside: x = ${x}`);
}

let num = 42;
tryToChange(num);
console.log(`Outside: num = ${num}`);  // Still 42!

// Objects and arrays: reference is shared
function addItem(items) {
    items.push("new item");
    console.log(`Inside: ${items}`);
}

let myList = ["a", "b"];
addItem(myList);
console.log(`Outside: ${myList}`);  // ['a', 'b', 'new item']

// Same rule: primitives are copied, objects/arrays are shared
// Value types (int, double, bool, struct): copied
static void TryToChange(int x)
{
    x = 999;
    Console.WriteLine($"Inside: x = {x}");
}

int num = 42;
TryToChange(num);
Console.WriteLine($"Outside: num = {num}");  // Still 42!

// Reference types (arrays, lists, objects): reference shared
static void AddItem(List<string> items)
{
    items.Add("new item");
    Console.WriteLine($"Inside: [{string.Join(", ", items)}]");
}

var myList = new List<string> { "a", "b" };
AddItem(myList);
Console.WriteLine($"Outside: [{string.Join(", ", myList)}]");
// [a, b, new item] — changed!

// C# also has explicit ref and out keywords:
static void DoubleIt(ref int x) { x *= 2; }

int value = 5;
DoubleIt(ref value);
Console.WriteLine(value);  // 10 — actually changed!

⚠️ Mutating Arguments Is a Common Bug Source

If a function modifies a list or object that was passed to it, the caller's data changes too. This can be surprising. To be safe, either document that your function modifies its input, or work on a copy inside the function. We'll revisit this in more depth when we cover collections in Module 5.

🎓 Instructor Note: Delivery Guidance

Pass-by-value vs. reference is conceptually tricky for beginners. Use a physical analogy: passing a number is like giving someone a photocopy of a document (they can scribble on it — your original is fine). Passing a list is like giving someone the address of your house (they can rearrange the furniture and you'll notice). Keep the explanation practical — this section plants the seed, and Module 5 (Collections) will reinforce it with more examples.

Exercises

🏋️ Exercise 1: Flexible Greeting

Objective: Write a function greet that accepts a name (required), a greeting (default: "Hello"), and a punctuation mark (default: "!").

It should return the formatted string, e.g., "Hello, Alice!"

Test with: just a name, a name + custom greeting, and all three arguments.

✅ Solution
def greet(name, greeting="Hello", punctuation="!"):
    return f"{greeting}, {name}{punctuation}"

print(greet("Alice"))                           # Hello, Alice!
print(greet("Bob", "Good morning"))             # Good morning, Bob!
print(greet("Charlie", "Hey", "..."))           # Hey, Charlie...
print(greet("Diana", punctuation="!!!"))        # Hello, Diana!!!
function greet(name, greeting = "Hello", punctuation = "!") {
    return `${greeting}, ${name}${punctuation}`;
}

console.log(greet("Alice"));
console.log(greet("Bob", "Good morning"));
console.log(greet("Charlie", "Hey", "..."));
static string Greet(string name, string greeting = "Hello",
    string punctuation = "!")
{
    return $"{greeting}, {name}{punctuation}";
}

Console.WriteLine(Greet("Alice"));
Console.WriteLine(Greet("Bob", "Good morning"));
Console.WriteLine(Greet("Charlie", "Hey", "..."));
Console.WriteLine(Greet("Diana", punctuation: "!!!"));

🏋️ Exercise 2: Sum Any Amount of Numbers

Objective: Write a function that accepts any number of arguments and returns their sum. Handle the case where no arguments are passed (return 0).

✅ Solution
def sum_all(*numbers):
    return sum(numbers)

print(sum_all())                # 0
print(sum_all(5))               # 5
print(sum_all(1, 2, 3))         # 6
print(sum_all(10, 20, 30, 40))  # 100

# Bonus: unpack a list into the function
scores = [88, 92, 75, 100]
print(sum_all(*scores))         # 355
function sumAll(...numbers) {
    return numbers.reduce((sum, n) => sum + n, 0);
}

console.log(sumAll());                // 0
console.log(sumAll(5));               // 5
console.log(sumAll(1, 2, 3));         // 6
console.log(sumAll(10, 20, 30, 40));  // 100

// Bonus: spread an array into the function
let scores = [88, 92, 75, 100];
console.log(sumAll(...scores));       // 355
static int SumAll(params int[] numbers)
{
    return numbers.Sum();
}

Console.WriteLine(SumAll());                // 0
Console.WriteLine(SumAll(5));               // 5
Console.WriteLine(SumAll(1, 2, 3));         // 6
Console.WriteLine(SumAll(10, 20, 30, 40));  // 100

// Pass an array directly
int[] scores = { 88, 92, 75, 100 };
Console.WriteLine(SumAll(scores));          // 355

🏋️ Exercise 3: Order Price Calculator

Objective: Write a function that calculates an order total with optional tax rate (default 0%), discount percentage (default 0%), and tip percentage (default 0%).

  • Start with subtotal (required)
  • Apply discount first, then add tax, then add tip
  • Use early returns to validate that subtotal is positive
  • Return the final total
✅ Solution
def calculate_total(subtotal, tax_rate=0, discount_pct=0, tip_pct=0):
    if subtotal <= 0:
        return "Error: Subtotal must be positive"

    # Apply discount
    discounted = subtotal * (1 - discount_pct / 100)

    # Apply tax
    with_tax = discounted * (1 + tax_rate / 100)

    # Apply tip (on pre-tax amount, common convention)
    tip = discounted * (tip_pct / 100)

    total = with_tax + tip
    return round(total, 2)

# Examples
print(calculate_total(100))                          # 100.0
print(calculate_total(100, tax_rate=8.5))             # 108.5
print(calculate_total(100, tax_rate=8.5, discount_pct=10))  # 97.65
print(calculate_total(50, tax_rate=8.5, tip_pct=20))  # 64.25
function calculateTotal(subtotal, {
    taxRate = 0,
    discountPct = 0,
    tipPct = 0
} = {}) {
    if (subtotal <= 0) return "Error: Subtotal must be positive";

    let discounted = subtotal * (1 - discountPct / 100);
    let withTax = discounted * (1 + taxRate / 100);
    let tip = discounted * (tipPct / 100);

    return Math.round((withTax + tip) * 100) / 100;
}

console.log(calculateTotal(100));
console.log(calculateTotal(100, { taxRate: 8.5 }));
console.log(calculateTotal(100, { taxRate: 8.5, discountPct: 10 }));
console.log(calculateTotal(50, { taxRate: 8.5, tipPct: 20 }));
static decimal CalculateTotal(decimal subtotal,
    decimal taxRate = 0, decimal discountPct = 0, decimal tipPct = 0)
{
    if (subtotal <= 0) return -1; // Error indicator

    decimal discounted = subtotal * (1 - discountPct / 100);
    decimal withTax = discounted * (1 + taxRate / 100);
    decimal tip = discounted * (tipPct / 100);

    return Math.Round(withTax + tip, 2);
}

Console.WriteLine(CalculateTotal(100));
Console.WriteLine(CalculateTotal(100, taxRate: 8.5m));
Console.WriteLine(CalculateTotal(100, taxRate: 8.5m, discountPct: 10));
Console.WriteLine(CalculateTotal(50, taxRate: 8.5m, tipPct: 20));
🎓 Instructor Note: Delivery Guidance

Exercise 1 is a warm-up for defaults. Exercise 2 introduces variable-length args with a clean, relatable use case. Exercise 3 is the star — it combines defaults, keyword/named arguments, early returns, and real math into a practical function. Note the JavaScript options object pattern in Exercise 3 — this is how real JS libraries handle many-parameter functions. Challenge fast students to add a currency parameter with formatting, or to break the calculator into smaller helper functions.

Summary

🎉 Key Takeaways

  • Default parameters provide fallback values — all three languages support them with = syntax
  • Python has built-in keyword arguments; C# has named arguments; JavaScript uses the options object pattern
  • Variable-length arguments: Python's *args/**kwargs, JavaScript's ...rest, C#'s params
  • C# has method overloading (same name, different parameter lists); Python/JS use defaults and type checks instead
  • Early returns (guard clauses) make functions flatter and easier to read than deep nesting
  • Primitives are passed by value (copied); objects/arrays are passed by reference (shared) — be careful modifying passed-in collections

🚀 What's Next?

You can now write flexible, reusable functions. The next lesson tackles one of the most important (and sometimes confusing) concepts in programming: scope and closures — where variables live and die, and how inner functions can "remember" their surroundings.

🎯 Quick Check

Question 1: What happens when you call a function without providing a default parameter's argument?

Question 2: In Python, what collects extra positional arguments into a tuple?