Python Refresher for Data Scientists
*args/**kwargs, and f-strings — that show up constantly once you start writing real data science code.
Why This Refresher Matters
NumPy, Pandas, and Scikit-Learn are all just Python libraries — every method you call and every result you get back is still built from ordinary Python: lists, dicts, functions, loops. The data science libraries lean especially hard on a handful of Python features, so it's worth making sure they're second nature before moving on.
Lists — Quick Review
A list is an ordered, mutable collection. You'll use lists constantly — as the raw material you turn into NumPy arrays and Pandas columns.
# Creating and indexing scores = [88, 92, 75, 61, 99] print(scores[0]) # 88 — first item print(scores[-1]) # 99 — last item # Slicing print(scores[1:3]) # [92, 75] — index 1 up to (not including) 3 print(scores[:2]) # [88, 92] — first two print(scores[::-1]) # reversed copy of the list # Common methods scores.append(70) # add to the end scores.sort() # sort in place, ascending print(len(scores)) # number of items print(sum(scores)) # total of all items print(max(scores), min(scores))
List comprehensions
A list comprehension builds a new list from an existing iterable in a single, readable line. You'll see this pattern everywhere in data cleaning code.
# The classic pattern: [expression for item in iterable] nums = [1, 2, 3, 4, 5] squares = [n ** 2 for n in nums] print(squares) # [1, 4, 9, 16, 25] # Adding a filter condition evens = [n for n in nums if n % 2 == 0] print(evens) # [2, 4] # Cleaning a messy list of names in one line raw_names = [" Alice", "BOB ", "charlie"] clean_names = [name.strip().title() for name in raw_names] print(clean_names) # ['Alice', 'Bob', 'Charlie']
for loop is usually the better choice.Dictionaries — Quick Review
A dict maps keys to values. In data science, dicts show up as configuration, as lookup tables, and as the shape JSON data naturally takes once you load it.
# Creating and accessing player = {"name": "Amara", "team": "Falcons", "goals": 14} print(player["name"]) # "Amara" print(player.get("assists", 0)) # 0 — safe default if key is missing # Adding / updating player["assists"] = 6 player["goals"] += 1 # Looping over a dict for key, value in player.items(): print(f"{key}: {value}") # Dict comprehension — build a lookup table in one line players = ["Amara", "Ben", "Cleo"] name_lengths = {name: len(name) for name in players} print(name_lengths) # {'Amara': 5, 'Ben': 3, 'Cleo': 4}
player["assists"] raises a KeyError and stops your program. player.get("assists", 0) returns a safe default instead — invaluable when working with real-world data where fields are sometimes missing.Functions
Functions package up repeatable logic — cleaning steps, calculations, formatting — so you can name a piece of work once and reuse it everywhere.
def celsius_to_fahrenheit(celsius): return celsius * 9 / 5 + 32 print(celsius_to_fahrenheit(100)) # 212.0 # A default argument def normalize_score(score, max_score=100): return score / max_score print(normalize_score(45)) # 0.45 — uses default max_score print(normalize_score(9, 10)) # 0.9 — overrides the default
Lambda Functions
A lambda is a small, anonymous, one-expression function. On its own it isn't especially interesting — but it's the idiom you'll reach for constantly when sorting data or passing a quick transformation into a Pandas method like .apply().
# A regular function... def square(n): return n * n # ...vs. the same thing as a lambda square_lambda = lambda n: n * n print(square_lambda(5)) # 25 # Where lambdas actually shine: sorting by a custom key players = [{"name": "Amara", "goals": 14}, {"name": "Ben", "goals": 9}, {"name": "Cleo", "goals": 21}] top_scorers = sorted(players, key=lambda p: p["goals"], reverse=True) print(top_scorers[0]["name"]) # "Cleo" — the top scorer # A preview of how this looks with pandas (covered in Section 2) # df["goals_doubled"] = df["goals"].apply(lambda g: g * 2)
def function instead and give it a name. Lambdas are for small, throwaway expressions — usually passed directly as an argument to another function.Unpacking, *args, and **kwargs
These three idioms come up constantly once you start reading other people's data science code, even if you don't write them yourself every day.
Unpacking
# Unpacking a list into named variables point = [10, 20] x, y = point print(x, y) # 10 20 # Skipping values with an underscore, and "gathering the rest" with * scores = [88, 92, 75, 61] first, *rest = scores print(first) # 88 print(rest) # [92, 75, 61] # Unpacking is exactly what makes this pattern from Section 3 work: # for key, value in player.items():
*args and **kwargs (briefly)
A function that needs to accept a flexible number of arguments uses *args (extra positional arguments, collected into a tuple) and **kwargs (extra keyword arguments, collected into a dict). You'll mostly recognize these in library function signatures rather than write them yourself early on.
def summarize(*args, **kwargs): print("positional:", args) print("keyword:", kwargs) summarize(88, 92, 75, source="quiz", graded=True) # positional: (88, 92, 75) # keyword: {'source': 'quiz', 'graded': True}
f-strings for Formatting Output
An f-string embeds expressions directly inside a string, prefixed with f. It's the standard way to build readable output — summaries, log lines, chart titles — in modern Python.
name = "Amara" goals = 14 avg = 2.3333 # Basic embedding print(f"{name} scored {goals} goals this season.") # Formatting numbers: 2 decimal places print(f"Average per game: {avg:.2f}") # "Average per game: 2.33" # Padding and alignment — handy for lining up a quick text table for n, g in [("Amara", 14), ("Ben", 9)]: print(f"{n:<10} {g:>3}") # Amara 14 # Ben 9
"Score: " + str(score) works, but breaks the moment a value isn't already a string, and it gets unreadable fast with several values. f"Score: {score}" handles the conversion for you and stays readable no matter how many values you add.Lesson Summary
Let's recap everything you learned in this lesson:
[expr for item in iterable if condition] — transform or filter a list in one line..get(key, default) to avoid KeyError.key= argument to sorted() or inside .apply().*args/**kwargs let a function accept a flexible number of arguments.f"...{value}...") are the standard, readable way to build formatted output.[n * 2 for n in [1, 2, 3]] evaluate to?my_dict.get("key", 0) instead of my_dict["key"]?first, *rest = [10, 20, 30, 40], what is rest?Combine several idioms from this lesson into one small script.
You're given a list of dicts, each representing one student's quiz score out of 20. Write a script that: (1) uses a list comprehension to build a list of each student's percentage score, (2) uses
sorted() with a lambda key to find the top scorer, and (3) prints a formatted report using f-strings.
{"name": "Zara", "score": 18},
{"name": "Malik", "score": 15},
{"name": "Priya", "score": 20},
]
Rules: Percentage =
score / 20 * 100. Print each student's name and percentage (2 decimal places), then print a final line announcing the top scorer.
💡 Show hints if you're stuck
- Comprehension:
[s["score"] / 20 * 100 for s in students] - Top scorer:
sorted(students, key=lambda s: s["score"], reverse=True)[0] - f-string:
f"{s['name']}: {pct:.2f}%"