S
Saurav Danej
90-Day AI/ML LinkedIn Content System
← All days
6
Day 6 of 90Python

Functions — the building block you'll write a million times

POST 1 of 5 MorningPythonConcept

A function is a value in Python

Coming from Java or C++, the idea that 'a function is a value' takes some adjustment. In those languages, methods belong to classes, classes are first-class, methods aren't.

In Python, functions are first-class. That means a function — the def add(a, b): return a + b you wrote — is itself a value. It has a name, a type (function), and you can pass it around exactly like an int or a string.

Let me show you. Define add, sub, mul. Now do this:

ops = [add, sub, mul]
for op in ops:
    print(op(2, 3))

You just iterated over functions and called each one. The interpreter didn't blink, because to it, ops is just a list of three values that happen to be callable.

This is the foundational fact behind a stack of features that show up in every Python codebase you'll ever read.

Decorators (Day 10) work because @my_decorator is shorthand for 'pass this function to my_decorator and replace it with the result'. That requires functions to be values.

Callbacks work — the on_click handlers in your UI library, the key= argument in sorted(), the loss function you pass into PyTorch's training loop — all of them depend on this.

Higher-order functions like map, filter, reduce work for the same reason.

This isn't a 'cool feature'. It's the bedrock for everything that makes modern Python ergonomic. Embrace it. Stop putting everything in classes (a Java reflex). Start writing small composable functions — they pass around freely, they test in isolation, they compose without ceremony.

Functions are first-class. Internalise it once and a lot of Python suddenly clicks.
#AI#MachineLearning#Python#100DaysOfCode#BuildInPublic#FunctionalProgramming#PythonOOP
POST 2 of 5 MiddayPythonDeep dive

Args, kwargs, and the order that matters

Python's function signature has way more dials than people use, and learning them turns 'serviceable' functions into well-designed APIs.

The full shape, in one signature:

def f(a, b, /, c, d, *, e, f):
    ...

The forward slash and asterisk are signature markers, not parameters. They divide the parameters into three zones.

Before the / are positional-only. Callers MUST pass them by position; they cannot pass them by keyword. Useful for the first one or two arguments where keyword names would be noise — open(path, mode), pow(base, exp).

Between / and * are flexible — callers may pass by position or by keyword. This is the default zone for most parameters.

After the * are keyword-only. Callers MUST pass them by keyword. This is invaluable for booleans and 'optional' configuration.

Why lock down the call style?

Force keyword-only for booleans. Compare f(True) to f(dry_run=True). The first reads like a code golf challenge; the second is self-documenting. Adding *, dry_run=False to your signature forces every caller to label what True or False means.

Force positional-only for required arguments. open('/etc/hosts', 'r') reads naturally. open(path='/etc/hosts', mode='r') is verbose noise.

The modern Python idiom for any function with two or more booleans, or any flag with non-obvious meaning, is to put it after a *. Most stdlib functions do this. Most popular libraries do this. The result is calling code that reads as English.

Next time you're designing a function, spend an extra 30 seconds on the signature. Decide which arguments must be positional, which must be keyword, which can be either. The function's lifetime users will thank you.
#AI#MachineLearning#Python#100DaysOfCode#BuildInPublic#PythonAdvanced#APIDesign
POST 3 of 5 AfternoonPythonCode

*args, **kwargs — what they actually do

*args and **kwargs cause more confusion than any other Python feature, and the confusion is purely about syntax overlap. There are two different operations using the same symbols.

In a function definition, * and ** are *packing*. They collect leftover positional arguments into a tuple, and leftover keyword arguments into a dict.

def log(level, *messages, **fields):
    print(level, messages, fields)

Now log('INFO', 'saved', 'ok', user='sd', id=7) prints:
INFO ('saved', 'ok') {'user': 'sd', 'id': 7}

The positional 'saved' and 'ok' got packed into messages. The keyword user= and id= got packed into fields. The required level got bound by position.

In a function call, * and ** are *unpacking*. They take a sequence or a dict and spread it into positional/keyword arguments.

args = ('INFO', 'saved')
kwargs = {'user': 'sd'}
log(*args, **kwargs)

This is the same as log('INFO', 'saved', user='sd'). The tuple gets spread; the dict gets spread.

You'll see this pattern everywhere. Decorators use *args, **kwargs to forward arbitrary arguments to the wrapped function. Class hierarchies use **kwargs in __init__ to pass through to super(). Test fixtures spread parametrized arguments into test functions.

Don't overuse them in your OWN code. Explicit named parameters are more readable, easier to type-check, easier to refactor. Save *args/**kwargs for the few specific situations where you genuinely need to accept anything — wrappers, forwarders, decorators.

Know them, recognise them, use them when justified, but default to explicit named parameters in the functions you write.
#AI#MachineLearning#Python#100DaysOfCode#BuildInPublic#PythonInternals#PythonAdvanced
POST 4 of 5 EveningPythonTip

Lambdas — short but rarely the right answer

Every Python tutorial introduces lambda eventually, and most beginners walk away thinking they should use it everywhere. They should not.

A lambda is an anonymous one-line function. lambda x: x * x is shorthand for an unnamed function returning x squared. The whole expression IS the function — no name, no def statement, no return keyword.

There are exactly two situations where lambda is the right call.

First, as a one-shot key argument to a built-in. sorted(people, key=lambda p: p.age) is clean. The function exists for one purpose, used once, and naming it square or get_age would be ceremony for no benefit. Same with filter(lambda x: x.ok, items), max(items, key=lambda i: i.score), and similar.

Second, in DSLs and frameworks that explicitly take callables. PyTorch's data transformations, pandas' .apply(), Django's URL configs sometimes. The framework expects a function; you supply one inline.

Where lambda is the WRONG call.

If you're saving the lambda to a name — square = lambda x: x * x — just write def square(x): return x * x. You get a proper function with a meaningful __name__ in stack traces, a docstring slot, and the ability to add type hints.

If the lambda is more than one line of logic — too late, it can't be. Multi-statement lambdas don't exist in Python by design. The moment your lambda needs an if-else (a ternary still works, but if you need branching), use def.

If the lambda is hard to read — Python's lambda has no extras. No statements, no annotations, no assignments. The constraint pushes you toward def for anything non-trivial.

Lambdas are tactical. Use sparingly, and only as one-shot keys.
#AI#MachineLearning#Python#100DaysOfCode#BuildInPublic#CleanCode#PythonStyle
POST 5 of 5 NightPythonRecap

Day 6 — functions are values

End of Day 6. Thirty posts in. Tomorrow we close out week 1.

What we covered.

Morning, the foundational shift — functions in Python are first-class values. You can store them in lists, pass them as arguments, return them from other functions. This single fact is the bedrock under decorators, callbacks, higher-order functions, and most of the framework code you'll ever read. If you came from Java or C++, this can take a week to actually feel natural. Worth the rewiring.

Midday, function signatures with intent. The / and * markers split parameters into positional-only, flexible, and keyword-only zones. Use this to make booleans explicit (force keyword-only) and to keep required arguments terse (allow positional-only). It's a half-hour of API design that pays back across the function's lifetime.

Afternoon, the *args/**kwargs split — packing in definitions, unpacking in calls. Same syntax, opposite operations. Recognise both, use them when forwarding, but default to explicit named parameters in your own code.

Evening, lambda discipline. Two real uses (one-shot keys, framework callbacks), no other excuses. Anything else, write a def. Stack traces, debuggers, and your future-self all thank you.

A week-one perspective check. We've covered: sprint setup, env tooling, types, control flow, comprehensions, functions. That's a solid Python core. By tomorrow we'll wrap with a recap and the reading list I'm using. By next Monday we're into OOP, decorators, generators — week 2 territory.

If you're following along with code, your Day 1-6 GitHub repo should have something running for at least four of the days by now. Don't worry about polish. Worry about 'does this run on my laptop'.

Tomorrow, Day 7 — week 1 wrap, the seven biggest lessons in seven posts, plus my exact reading list for the next 83 days.
#AI#MachineLearning#Python#100DaysOfCode#BuildInPublic#PythonFunctions#DailyRecap