What does ** (double star/asterisk) and * (star/asterisk) do for parameters?

In Python, the double star (**) is used to denote an "unpacking" operator, which allows you to unpack a dictionary or other iterable data type into keyword arguments in a function call. For example, consider the following function:

def greet(greeting, recipient):
    return f"{greeting}, {recipient}!"

print(greet('Hello', 'World'))

This function takes two arguments: greeting and recipient. If we have a dictionary that contains these values, we can "unpack" them into the function call like this:

def greet(greeting, recipient):
    return f"{greeting}, {recipient}!"

data = {'greeting': 'Hello', 'recipient': 'world'}
print(greet(**data))

This would return the string "Hello, world!".

The single star (*) is used to denote the "packing" operator, which allows you to pack a sequence (such as a list or tuple) into individual elements in a function call. For example:

def greet(greeting, recipient):
    return f"{greeting}, {recipient}!"

greetings = ['Hello', 'Hi', 'Hola']
recipients = ['world', 'Python', 'everyone']

for g, r in zip(greetings, recipients):
    print(greet(g, r))

This would print the following:

Hello, world!
Hi, Python!
Hola, everyone!

The zip function returns an iterator of tuples, where each tuple contains one element from each of the input iterables. In this case, we are using the * operator to "unpack" each tuple into separate arguments in the function call.