Skip to content

Tutorial 3.2: Functions

Time: ~15 minutes Prerequisites: Tutorial 3.1: Variables and Types


What is a Function?

A function is like a recipe: a set of instructions with a name. You can use the recipe over and over without writing the instructions each time.

    Real World:                      In Python:

    ┌─────────────────────────┐     def make_sandwich():
    │   MAKE A SANDWICH       │         get_bread()
    │                         │         add_peanut_butter()
    │   1. Get bread          │         add_jelly()
    │   2. Add peanut butter  │         put_together()
    │   3. Add jelly          │
    │   4. Put together       │
    └─────────────────────────┘

Instead of writing all 4 steps every time, you just say make_sandwich().

Creating a Function

The basic pattern is:

def function_name():
    # Code inside the function
    # (indented with 4 spaces)
    do_something()
    do_something_else()

Example:

def say_hello():
    print("Hello, World!")
    print("I am a VEX robot!")

Function Anatomy Flowchart

flowchart TD
    subgraph "Function Definition"
        A["def"] --> B["function_name"]
        B --> C["( )"]
        C --> D[":"]
    end
    E["    indented code"] --> F["Part of the function"]

    style A fill:#e8f5e9,stroke:#2e7d32
    style B fill:#bbdefb,stroke:#1565c0
    style C fill:#fff9c4,stroke:#f57f17
    style D fill:#f8bbd9,stroke:#c2185b
Part Meaning
def Keyword that starts a function definition
function_name The name you give your function
( ) Parentheses hold parameters (inputs)
: Colon marks the start of the function body
indented code The instructions inside the function (4 spaces)

Using (calling) the function:

say_hello()  # This runs both print statements

Output:

Hello, World!
I am a VEX robot!

Function Call Flowchart

When you call a function, the computer "jumps" to run that function's code, then returns:

flowchart TD
    A["Your code runs..."] --> B["say_hello()"]
    B --> C["Jump to function definition"]
    C --> D["Execute: print('Hello, World!')"]
    D --> E["Execute: print('I am a VEX robot!')"]
    E --> F["Return to calling code"]
    F --> G["Continue with next line"]

    style B fill:#fff9c4,stroke:#f57f17
    style C fill:#e3f2fd,stroke:#1565c0
    style F fill:#c8e6c9,stroke:#2e7d32

Parameters: Giving Functions Information

Parameters are like ingredients - information you give to the function:

    Real World:                      In Python:

    MAKE SANDWICH with:              def make_sandwich(bread_type, filling):
    - Bread type: white                  get_bread(bread_type)
    - Filling: turkey                    add_filling(filling)
def greet(name):
    print("Hello, " + name + "!")

greet("Alice")   # Output: Hello, Alice!
greet("Bob")     # Output: Hello, Bob!

Parameters and Arguments Flowchart

flowchart LR
    subgraph "Function Call"
        A["greet('Alice')"] --> B["'Alice' is the ARGUMENT"]
    end
    subgraph "Function Definition"
        C["def greet(name):"] --> D["name is the PARAMETER"]
    end
    B --> D
    D --> E["name = 'Alice' inside function"]

    style A fill:#fff9c4,stroke:#f57f17
    style B fill:#e8f5e9,stroke:#2e7d32
    style D fill:#bbdefb,stroke:#1565c0

Remember: - Parameter = the variable in the function definition (placeholder) - Argument = the actual value you pass when calling (the real data)

Multiple Parameters

def introduce(name, age, team):
    print("I am " + name)
    print("I am " + str(age) + " years old")
    print("I am on team " + str(team))

introduce("VEX Bot", 1, 12345)

Output:

I am VEX Bot
I am 1 years old
I am on team 12345

Return Values: Getting Information Back

Functions can give you results back using return:

    Real World:                  In Python:

    CALCULATOR:                  def add(a, b):
    "What's 5 + 3?"                  result = a + b
    "The answer is 8"                return result

Return Value Flowchart

flowchart LR
    A["add(5, 3)"] --> B["a=5, b=3"]
    B --> C["result = 5 + 3"]
    C --> D["return 8"]
    D --> E["sum = 8"]

    style A fill:#fff9c4,stroke:#f57f17
    style D fill:#e8f5e9,stroke:#2e7d32
    style E fill:#bbdefb,stroke:#1565c0
def add(a, b):
    result = a + b
    return result

# Use the returned value
sum = add(5, 3)
print(sum)  # Output: 8

Why Return Matters

# WITHOUT return - you can't use the result
def add_no_return(a, b):
    result = a + b
    print(result)  # Just prints, doesn't give back

x = add_no_return(5, 3)  # x is None!

# WITH return - you can use the result
def add_with_return(a, b):
    result = a + b
    return result

y = add_with_return(5, 3)  # y is 8!
print(y * 2)  # You can use it! Output: 16

Code Connection: Functions in utils.py

Let's examine a real function from src/utils.py:

def deadband(value, threshold=5.0):
    """
    Apply deadband to joystick input to prevent drift.

    Args:
        value: The input value (typically -100 to 100)
        threshold: Values below this are treated as zero

    Returns:
        0 if within deadband, otherwise the original value
    """
    if abs(value) < threshold:
        return 0.0
    return value

Let's break this down:

    def deadband(value, threshold=5.0):
    ↑      ↑            ↑
    |      |            |
    |      |            Default value (optional parameter)
    |      Function name
    Keyword to define a function

    if abs(value) < threshold:
       Built-in function: absolute value (makes negative positive)

    return 0.0
           Give back zero if input is small

    return value
           Otherwise, give back original

deadband() Function Flowchart

flowchart TD
    A["deadband(value, threshold=5)"] --> B{"Is |value| < threshold?"}
    B -->|"Yes: Small input\n(joystick drift)"| C["return 0.0\n(ignore it)"]
    B -->|"No: Real input\n(driver moving)"| D["return value\n(use it)"]

    subgraph "Push Back Examples"
        E["Joystick at 3%"] --> F["deadband(3) → 0"]
        G["Joystick at 50%"] --> H["deadband(50) → 50"]
    end

    style C fill:#ffcdd2,stroke:#c62828
    style D fill:#c8e6c9,stroke:#2e7d32
    style F fill:#fff9c4,stroke:#f57f17
    style H fill:#c8e6c9,stroke:#2e7d32

Using deadband()

# Normal use
result1 = deadband(50)     # Returns 50 (above threshold)
result2 = deadband(3)      # Returns 0 (below 5 threshold)
result3 = deadband(-4)     # Returns 0 (abs(-4) = 4 < 5)

# With custom threshold
result4 = deadband(8, threshold=10)   # Returns 0 (below 10)
result5 = deadband(15, threshold=10)  # Returns 15 (above 10)

Default Parameters

Notice threshold=5.0? That's a default parameter:

# If you don't specify threshold, it uses 5.0
deadband(50)              # Same as deadband(50, 5.0)

# But you CAN specify a different value
deadband(50, 10)          # Uses 10 as threshold
deadband(50, threshold=10) # Same thing, more readable

More Functions from utils.py

clamp()

def clamp(value, min_val, max_val):
    """Clamp a value between minimum and maximum bounds."""
    return max(min_val, min(value, max_val))

# Examples:
clamp(150, 0, 100)   # Returns 100 (capped at max)
clamp(-50, 0, 100)   # Returns 0 (raised to min)
clamp(50, 0, 100)    # Returns 50 (already in range)

curve_input()

def curve_input(value, exponent=2.0):
    """Apply exponential curve for finer control at low speeds."""
    sign = 1 if value >= 0 else -1
    normalized = abs(value) / 100.0
    curved = (normalized ** exponent) * 100.0
    return sign * curved

# Examples:
curve_input(50)      # Returns 25 (50% input → 25% output)
curve_input(100)     # Returns 100 (max stays max)
curve_input(-50)     # Returns -25 (preserves direction)

Why is this useful? It gives more precise control at low speeds!

curve_input() Flowchart

flowchart TD
    subgraph "Why Curve Input for Push Back?"
        A["Precise block\npositioning"] --> B["Low speed =\nfine control"]
        C["Fast field\ncrossing"] --> D["High speed =\nfull power"]
    end

    subgraph "curve_input(50) calculation"
        E["50% joystick"] --> F["normalize: 0.5"]
        F --> G["square: 0.25"]
        G --> H["scale: 25% output"]
    end

    style B fill:#c8e6c9,stroke:#2e7d32
    style D fill:#fff3e0,stroke:#ef6c00
    style H fill:#bbdefb,stroke:#1565c0
    LINEAR (no curve):          CURVED (exponent=2):

    Joystick  →  Motor          Joystick  →  Motor
       0%     →    0%              0%     →    0%
      25%     →   25%             25%     →    6.25%  ← More precision!
      50%     →   50%             50%     →   25%
      75%     →   75%             75%     →   56.25%
     100%     →  100%            100%     →  100%

Why this matters for Push Back: When you're carefully pushing blocks into a goal, you need precise, slow movements. curve_input() lets you make small adjustments without overshooting!

Functions Calling Functions

Functions can use other functions:

def process_joystick(raw_value):
    """Process raw joystick input with deadband and curve."""
    # First, apply deadband
    after_deadband = deadband(raw_value)

    # Then, apply curve
    final_value = curve_input(after_deadband)

    return final_value

# Use it:
speed = process_joystick(25)  # Goes through both processing steps

The Docstring

The text in triple quotes """...""" is called a docstring:

def deadband(value, threshold=5.0):
    """
    Apply deadband to joystick input to prevent drift.

    Args:
        value: The input value (typically -100 to 100)
        threshold: Values below this are treated as zero

    Returns:
        0 if within deadband, otherwise the original value
    """

It explains what the function does. Good practice!


Summary

Concept What It Means Example
Function Reusable set of instructions def say_hello():
Parameter Input to a function def greet(name):
Return Output from a function return result
Default Optional parameter with preset value threshold=5.0
Calling Using a function say_hello()
Docstring Documentation in """...""" Explains the function

Exercise: Write Your Own Functions

Challenge 1: Write a function that calculates the area of a rectangle:

def rectangle_area(width, height):
    # Your code here
    pass

# Test it:
print(rectangle_area(5, 3))  # Should print 15

Challenge 2: Write a function that converts motor percent to RPM:

def percent_to_rpm(percent, max_rpm=200):
    # Your code here
    # Hint: 100% = max_rpm, 50% = half of max_rpm
    pass

# Test it:
print(percent_to_rpm(100))      # Should print 200
print(percent_to_rpm(50))       # Should print 100
print(percent_to_rpm(75, 600))  # Should print 450 (using blue cartridge max)

Challenge 3: Look at utils.py and try to understand scale_input(). What does it do?

Challenge 4 (Push Back): Write a function called parking_bonus that takes num_robots (0, 1, or 2) and returns the correct bonus points (0, 8, or 30).

Challenge 5 (Push Back): Write a function called check_awp_requirements that checks if an alliance meets the Autonomous Win Point requirements: - 7+ blocks scored - Blocks in 3+ different goals - 3+ blocks removed from loaders


Push Back Helper Functions

Here are specialized functions for the Push Back competition:

# ═══════════════════════════════════════════════════════════════
# PUSH BACK SCORING FUNCTIONS
# ═══════════════════════════════════════════════════════════════

def calculate_block_score(blocks):
    """
    Calculate points from blocks scored in Push Back.

    Args:
        blocks: Number of blocks pushed into goals

    Returns:
        Total block points (3 points per block)
    """
    POINTS_PER_BLOCK = 3
    return blocks * POINTS_PER_BLOCK


def check_zone_control(our_blocks, opponent_blocks):
    """
    Determine if we control a goal zone in Push Back.

    Args:
        our_blocks: Number of our alliance's blocks in the zone
        opponent_blocks: Number of opponent's blocks in the zone

    Returns:
        True if we have majority, False otherwise
    """
    return our_blocks > opponent_blocks


def get_zone_bonus(zone_type, have_control):
    """
    Get bonus points for controlling a goal zone.

    Args:
        zone_type: "long", "center_upper", or "center_lower"
        have_control: True if we have majority in the zone

    Returns:
        Bonus points for that zone (0 if no control)
    """
    if not have_control:
        return 0

    if zone_type == "long":
        return 10
    elif zone_type == "center_upper":
        return 8
    elif zone_type == "center_lower":
        return 6
    else:
        return 0


def parking_bonus(num_robots):
    """
    Calculate parking bonus based on robots parked.

    Args:
        num_robots: Number of alliance robots parked (0, 1, or 2)

    Returns:
        Parking bonus points
    """
    if num_robots == 2:
        return 30
    elif num_robots == 1:
        return 8
    else:
        return 0


def calculate_total_score(blocks, zone_controls, robots_parked):
    """
    Calculate complete alliance score for a Push Back match.

    Args:
        blocks: Total blocks scored
        zone_controls: List of (zone_type, have_control) tuples
        robots_parked: Number of robots in park zone (0, 1, or 2)

    Returns:
        Total match score
    """
    # Block points
    score = calculate_block_score(blocks)

    # Zone control bonuses
    for zone_type, have_control in zone_controls:
        score = score + get_zone_bonus(zone_type, have_control)

    # Parking bonus
    score = score + parking_bonus(robots_parked)

    return score


# ═══════════════════════════════════════════════════════════════
# USING THE FUNCTIONS
# ═══════════════════════════════════════════════════════════════

# Example match scenario
blocks_scored = 8
zone_controls = [
    ("long", True),           # We control a long goal: +10
    ("center_upper", False),  # Opponent controls center: +0
]
robots_parked = 2             # Both robots parked: +30

# Calculate final score
final_score = calculate_total_score(blocks_scored, zone_controls, robots_parked)
print("Match Score: " + str(final_score))  # 24 + 10 + 30 = 64 points

Common Mistakes with Functions

Mistake 1: Forgetting Parentheses When Calling

# WRONG: This doesn't call the function!
result = add    # Just refers to the function object, doesn't run it

# RIGHT: Parentheses call the function
result = add(5, 3)  # Actually runs add() and returns 8

Mistake 2: Forgetting to Return

# WRONG: Function doesn't return anything useful
def add(a, b):
    result = a + b
    # Forgot return! result is calculated but lost

x = add(5, 3)    # x is None, not 8!

# RIGHT: Return the result
def add(a, b):
    result = a + b
    return result  # Now the value comes back

y = add(5, 3)    # y is 8

Mistake 3: Wrong Indentation

# WRONG: Code not indented inside function
def greet():
print("Hello")    # ERROR: IndentationError!

# RIGHT: Indent with 4 spaces
def greet():
    print("Hello")  # This is inside the function

Mistake 4: Missing Colon

# WRONG: Missing colon after function definition
def say_hello()   # SyntaxError!
    print("Hello")

# RIGHT: Colon marks start of function body
def say_hello():
    print("Hello")

Mistake 5: Modifying Parameters Doesn't Change Original

# This might surprise you:
def double(x):
    x = x * 2    # This only changes x inside the function
    return x

my_speed = 50
double(my_speed)          # Returns 100, but...
print(my_speed)           # Still 50! Original wasn't changed

# If you want to change the original:
my_speed = double(my_speed)  # Now my_speed is 100

How Functions Connect to Push Back

Functions organize your Push Back robot code into reusable pieces:

Function Purpose in Push Back
deadband() Stops joystick drift when positioning blocks precisely
clamp() Keeps motor speeds valid when using turbo mode
curve_input() Gives precise control for careful block placement
autonomous_routine() Runs your 15-second scoring sequence
driver_control_loop() Controls robot during 1:45 driver period
calculate_block_score() Tracks your alliance's points

Why Functions Matter

Without functions, you'd have to: 1. Copy the same code everywhere you need it 2. Fix bugs in multiple places 3. Make your code impossible to read

With functions, you: 1. Write code once, use it everywhere 2. Fix bugs in one place 3. Give meaningful names to complex operations

# Without functions: Confusing!
left_speed = controller.axis3.position()
if abs(left_speed) < 5:
    left_speed = 0
# ... repeated everywhere you need deadband

# With functions: Clear!
left_speed = deadband(controller.axis3.position())
# One line, easy to understand

Answers

Challenge 1:

def rectangle_area(width, height):
    area = width * height
    return area

Challenge 2:

def percent_to_rpm(percent, max_rpm=200):
    rpm = (percent / 100) * max_rpm
    return rpm

Challenge 3: scale_input() converts a value from one range to another. Example: convert a 0-1023 sensor reading to 0-100% output.


← Previous: Variables and Types | Next: Loops and Conditionals →