Python

Write an awesome doc for Python. A very nice an practical one extracted from Python official documentation.

View on GitHub

Functions in Python

[!NOTE]

If you need a refresher about what is a function in general in programming take a look at this.

Function definition in python

[!NOTE]

.append() is a method of list object result. .strip() is a method of string object here. .copy() and .items() is a method of dictionary object here.

Method is:

  • A function that “belongs” to an object and is named obj.method_name.
  • obj can be of type list, string, etc.
    • Different types define different methods. E.g. you do not have a strip method on a integer.

Later when we start talking about classes you’ll learn how to define your own object types and their methods :).

Default Values for Args

def calculate_salary(user, hour_rate=27):
    result = user["hours"] * hour_rate
    return result
user = { "hours": 15.2 }
salary = calculate_salary(user) # Or calculate_salary(user, hour_rate=29)
user["salary"] = salary
print(user)

[!TIP]

Best practice: Do not mutate the passed arguments by reference. This practice is commonly know as “avoiding side effects” and instead returning the new result.

Learn more about it here but I am gonna summarize it here in bullet points:

  1. Testability: Your code’s ability to be tested easily will drop significantly.
  2. Reusability: You’ll find it harder to not duplicated your code (DRY principle), but rather reuse it.
  3. Flexibility: It’ll be harder to change and adapt to new requirements.
  4. Readability: Just imagine yourself going up and down to comprehend what a function does and how it fits into the bigger picture.

[!CAUTION]

The default value is evaluated only once. Why does this matter?

Buggy code Improved version
https://github.com/kasir-barati/python/blob/ffb265fcbd407dc2873a2495b199ded3720c578e/02-getting-started/assets/default-value-evaluation.py#L1-L9 https://github.com/kasir-barati/python/blob/ffb265fcbd407dc2873a2495b199ded3720c578e/02-getting-started/assets/default-value-evaluation.py#L11-L20
Coalescing in python
  • Here we are leveraging short-circuit evaluation in logical expressions.
  • user = user if user is not None else {}
  • A much longer version but more traditional and familiar to the eyes:
    
          if user is None:
              user = {}
          

keyword_arg=value / kwarg=value

def add_to_cart(
        product,
        cart,
        quantity=1,
        app="website"):
    print(f"{quantity} {product}s has been added to your cart #{cart}.")
    print(f"Thanks for using our {app} :)")
add_to_cart('apple', 12, 65498) # positional argument
add_to_cart('tomato', cart=321564) # 1 positional, 1 keyword
add_to_cart(quantity=4, cart=321564, product='tomato') # keyword argument

# INVALID invocations
add_to_cart() # missing required args
add_to_cart(discount=123) # unknown keyword argument
add_to_cart(cart=123987, 'Pineapple') # positional argument after a keyword argument
add_to_cart('apple', 65498, cart=65498) # TypeError: add_to_cart() got multiple values for argument 'cart'

[!TIP]

# Use common sense!

Use common sense

For example this function definition is wrong:

Syntactically wrong function definition

BTW here we’re following PEP 8 for styling our code.

Functions with Variable Number of Arguments

def f1(a, b=False, **rest):
    print(a)
    print(b)
    print("-" * 20)
    for key, value in rest.items():
        print('\t', key, '\t: ', value)

# Wrong: f1(12,True, "hi", "fun", 123)
f1(12,True, greet="hi", r="fun", s=123)
f1(12, b=True, greet="hi", r="fun", s=123)
f1(12, greet="hi", r="fun", s=123)

print('/\\' * 20)
print(r'\/' * 20)

def greet(greet_message, *names):
    for name in names:
        print(greet_message, name)
        print("-" * 20)

# Wrong greet("こにちは", me="Mohammad Jawad", user={"name": "Hiroshi"}, pi=3.14)
greet("こにちは", "Mohammad san", "さくら", "Alex", "Hana", True)

[!TIP]

We’ve seen how to unpack tuples, dictionaries, and lists in match case here, now we’ll learn how to unpack a list/tuple in Python. Then we can pass the unpacked values as arguments to a function:

greet_args = ["Moin", "Christoph", "ひなた", "Jawad", "ちなつ"]
greet(*greet_args) # call with arguments unpacked from a list

And for dict you can add two asterisks: **:

add_to_cart_args = {
    "product": "Chopstick",
    "quantity": 1,
    "cart": 565676,
    "app": "android-app"
}
add_to_cart(**add_to_cart_args)

Dictates what your function accepts

Positional or keyword

def place_order(
        product_id,
        quantity,
        /,
        discount_code=None,
        *,
        shipping_address,
        expedited=False):
    print(f"Order Details:\n - Product ID: {product_id}\n - Quantity: {quantity}")
    if discount_code:
        print(f" - Discount Code: {discount_code}")
    print(f" - Shipping Address: {shipping_address}")
    print(f" - Expedited Shipping: {'Yes' if expedited else 'No'}")

# Correct Calls:
place_order(101, 2, "SAVE20", shipping_address="123 Elm St")  # Discount applied
place_order(202, 5, shipping_address="456 Oak St", expedited=True)  # No discount

# Incorrect Calls (will raise TypeError):
# place_order(product_id=101, quantity=2, shipping_address="789 Pine St")  # product_id and quantity are positional-only
# place_order(101, 2, "SUMMER", "789 Pine St")  # shipping_address must be a keyword argument

def pos_only_arg(arg, /):
    print(arg)
def kwd_only_arg(*, arg):
    print(arg)

[!TIP]

Back to “use common sense” we can tweak the function definition a little to do what we want. So the problem there is that:

  1. We have a collision between the positional argument name and **kwds which has name as a key.
  2. Keyword name will always bind to the first parameter.
def foo(name, /, **kwds):
    return 'name' in kwds

So by using / we’re declaring name as a positional argument and the second argument will be able to use name as a key for a dictionary without causing any ambiguity.

Final Words – a Guideline

YouTube/Aparat

Ref