DEV Community

Cover image for Lists as Function Interfaces: *args, Mutable Defaults, and the One-Liner Power
Aaron Rose
Aaron Rose

Posted on

Lists as Function Interfaces: *args, Mutable Defaults, and the One-Liner Power

Functions That Can Handle Surprises

Imagine you're running a lemonade stand. Most days, you know exactly how many customers you'll have. But some days are surprises! Python functions face the same challenge—sometimes they know how many arguments they'll get, sometimes they don't.

Meet Flexible Fiona, our function that can handle any number of customers:

def make_lemonade_for_group(*customers):
    """*customers lets Fiona handle any number of people!"""
    cups = []
    for customer in customers:
        cups.append(f"Lemonade for {customer}")
    return cups
Enter fullscreen mode Exit fullscreen mode

The asterisk * is like Fiona saying "I'm ready for surprises!" She can handle:

  • make_lemonade_for_group("Alice")
  • make_lemonade_for_group("Alice", "Bob", "Charlie")
  • Even make_lemonade_for_group() with no customers!

The Magical Asterisk: Unpacking Lists

But here's where it gets really powerful. What if you already have a list of customers?

regular_customers = ["Alice", "Bob", "Charlie"]

# Without the asterisk - Fiona gets confused!
make_lemonade_for_group(regular_customers)  # Thinks the whole list is one customer!

# With the asterisk - magic!
make_lemonade_for_group(*regular_customers)  # "Unpacks" the list into three customers
Enter fullscreen mode Exit fullscreen mode

The asterisk * says "take this list and spread it out like individual items." It's like opening a box of chocolates and placing each one separately on the counter.

The Dangerous Mutable Default Trap

Now, let me tell you about Forgetful Frank and his infamous lemonade pitcher. This is one of Python's most famous "gotchas"!

def get_lemonade_pitcher(flavor="lemon", ingredients=[]):  # DANGER!
    ingredients.append(flavor)
    return ingredients
Enter fullscreen mode Exit fullscreen mode

This looks innocent, but there's a hidden bug! Let's watch what happens:

# Morning batch
morning_pitcher = get_lemonade_pitcher("lemon")
print(morning_pitcher)  # ["lemon"] - Good!

# Afternoon batch  
afternoon_pitcher = get_lemonade_pitcher("lime")
print(afternoon_pitcher)  # ["lemon", "lime"] - Wait, what?!
Enter fullscreen mode Exit fullscreen mode

📊 Visualize This: Imagine Frank has one shared pitcher that he uses for every function call. The ingredients=[] creates the list once when Python reads the function definition, not each time the function is called!

The Safe Fix:

def get_lemonade_pitcher_safe(flavor="lemon", ingredients=None):
    if ingredients is None:  # Create a new list each time!
        ingredients = []
    ingredients.append(flavor)
    return ingredients
Enter fullscreen mode Exit fullscreen mode

Now Frank checks if you brought your own pitcher (ingredients). If not, he gets a fresh one each time!

The One-Liner Power: Thinking in Results

Intermediate developers think differently about return values. Instead of building results step-by-step, they think about the final shape they want.

Junior approach (step-by-step thinking)

def find_sweet_lemonades(lemonades):
    results = []
    for lemonade in lemonades:
        if lemonade.sweetness > 7:
            results.append(lemonade.name)
    return results
Enter fullscreen mode Exit fullscreen mode

Intermediate approach (result-focused thinking):

def find_sweet_lemonades(lemonades):
    return [lemonade.name for lemonade in lemonades if lemonade.sweetness > 7]
Enter fullscreen mode Exit fullscreen mode

But we can go even further with handling edge cases gracefully:

def find_sweet_lemonades_smart(lemonades):
    sweet_ones = [lemonade.name for lemonade in lemonades if lemonade.sweetness > 7]
    return sweet_ones if sweet_ones else ["No sweet lemonades today!"]
Enter fullscreen mode Exit fullscreen mode

The next() Superpower: Finding Just the First Match

Speaking of one-liners, here's a super-efficient trick for when you only need to find the first thing you're looking for:

def find_first_sweet_lemonade(lemonades):
    # Like searching until you find what you want, then stopping
    return next((lem for lem in lemonades if lem.sweetness > 7), None)
Enter fullscreen mode Exit fullscreen mode

This says: "Look through the lemonades one by one. When you find a sweet one, stop looking and return it. If you don't find any, return None." Much more efficient than processing the entire list!

Returning Structured Results: Be a Helpful API

Instead of just returning a flat list, think about what the caller really needs:

def analyze_lemonade_stand(lemonades):
    return {
        'total': len(lemonades),
        'sweet_count': sum(1 for lem in lemonades if lem.sweetness > 7),
        'names': [lem.name for lem in lemonades],
        'sweetest': max(lemonades, key=lambda lem: lem.sweetness).name
    }
Enter fullscreen mode Exit fullscreen mode

Now the caller gets a whole report card, not just a raw list! This makes your function much more useful to other developers (including future you).

The Takeaway: Functions That Communicate Clearly

The intermediate leap is about designing functions that:

  • Handle surprises gracefully with *args
  • Avoid hidden traps with mutable defaults
  • Return information in useful, structured formats
  • Express intent clearly through their interfaces

Remember: Good function design is about making life easier for the person calling your function. When you write clear, predictable functions, you're not just writing code—you're building a helpful API that others (and future you) will thank you for!


Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Top comments (0)