Prerequisite Knowledge#
Note
This is an introduction to a couple intermediate Python concepts that are useful for excelbird. Skip this page if: you already understand object-oriented-programming, comprehensions, and iterable unpacking.
Prerequisites (for this tutorial)#
You understand basic types: int, str, list, dict, and know how to use and create them
You can write a function that takes required and optional arguments (i.e.
def func(required, optional=None): ...) and know how to call that function.You understand loops and how to iterate through things
Key terms#
Argument and parameter
You’ll see these terms a lot. They refer to the same thing, but indicate the perspective from which we’re referring. For instance, function
def func(name): ...has one parameter,name. When you callfunc('Jeff')you’ve given “Jeff” as an argument forname. Arguments are the things given, and parameters are taken/expeceted.
Positional arguments versus keyword arguments
There are only two ways to pass an argument: By keyword, and by position. If we write,
func('Jeff'), we’ve passed Jeff’s name as a positional argument to func. If instead we write,func(name='Jeff'), we’ve given it a keyword argument
Naming things#
You may have noticed that some things are named in snake_case and others use TitleCase. This means something. TitleCase indicates the term is a class whose source code is written in Python. Pandas DataFrame is a Python class. Numpy’s ndarray is written in C
In excelbird, all layout element types are custom classes, not functions.
Objects and Classes#
If asked to define what a “car” means, you may write:
Car definition:
Has wheels
Has colored paint
Moves forward
You’ve just created a class. You defined a type of object. No car was created yet. You defined what attributes a car has, and how it should behave. Each time one is manufactured, you will have created an instance of Car, just like 1 is an instance of int, and 'hello' is an instance of str.
[35]:
class Car:
def __init__(self, color, wheel_size):
self.color = color
self.wheel_size = wheel_size
def move_forward(self):
print("moving forward")
my_car = Car('white', 20)
my_car.move_forward()
print(my_car.wheel_size)
moving forward
[35]:
20
Magic Methods#
Excelbird objects can handle being used in arithmetic expressions, like my_column * my_row / 5. How is this possible?
An object’s magic methods have pre-defined names, and get called automatically by Python when a certain event happens. Square brackets: my_list[5] is just shorthand for my_list.__getitem__(5). Python doesn’t care what ‘my_list’ is - it will just call __getitem__ when it sees the brackets. You can then customize that method however you want.
Math symbols work the same way: "py" + "thon" is just a shortcut for "py".__add__("thon"). Define custom logic for __add__, and you can break math.
[36]:
class Cell:
def __add__(self, other):
return other * 100
c = Cell()
c + 5
[36]:
500
Inline If-Else (Ternary)#
In Python, we can write if-else statements in a single line. In excelbird, this feature is necessary for being able to nest logic inside your layout, instead of writing it elsewhere.
[ ]:
age = 12
can_drink = True if age >= 21 else False
can_drink_message = (
"Yes" if age >= 21
else "No" if age >= 18
else "Not a chance!" if age >= 12
else "That's a seriously bad idea."
)
The rules are simple:
You must include an ‘else’ clause at the end of the statement
An ‘elif’ can be simulated by writing
else <value> if <condition>, and you can include as many of these as you want.
Where can I use an inline conditional?
Literally anywhere you’d use a variable. Since the inline conditional will always return a value, it’s safe to use it in the middle of a function call, or before accessing an instance method or attribute, as long as you surround the statement in parentheses
Excelbird Example#
Note
All excelbird layout elements accept None as a child argument, and immediately filter it out. This lets you make an inline decision on whether to display something
[ ]:
from excelbird import Book, Sheet, Cell
show_element = False
Book(
Sheet(
Cell(1),
Cell(2) if show_element is True else None,
Cell(3),
),
).write("test.xlsx")

List Comprehensions#
Comprehensions are a necessary skill for nesting inline logic in an excelbird layout. They’re easy to learn, but take some time to master. Just practice!
In other programming languages, you might write code that looks like:
[ ]:
items = []
for i in range(5):
items.append(i)
In Python, we can write…
[ ]:
items = [i for i in range(5)]
Comprehension Inline If-Else#
In the example above, there are two places we can apply nested logic
To the returned element
To the iterable we’re looping through.
[5]:
[i if i % 2 == 0 else None for i in range(5)] # option 1
[5]:
[0, None, 2, None, 4]
[6]:
[i for i in range(5) if i % 2 == 0] # option 2
[6]:
[0, 2, 4]
Examine the second example. We broke rule #1 of the inline-if/else discussion earlier: we didn’t include an ‘else’ clause.
When written in place of a value, inline-if statements determine which value to return, but when written after a sequence (like range(5)), they filter out elements returned by the sequence. Therefore, an ‘else’ clause serves no purpose when our only option is to filter out elements.
Iterable Unpacking#
This is a simple feature, but it might take a few minutes to wrap your head around.
Try placing a * or ** before an inline reference to something: *my_list or **my_dict.
Examine the following examples.
[9]:
list_without_unpacking = [
1,
[2, 3],
4,
]
for e in list_without_unpacking:
print(e)
1
[2, 3]
4
[4]:
list_with_unpacking = [
1,
*[2, 3],
4,
]
for e in list_with_unpacking:
print(e)
1
2
3
4
The * seems to have simulated the effect of passing each element in a list separately, instead of passing the list itself.
The unpacking happens immediately, so the receiver of your arguments has no idea you’ve unpacked anything
Here’s a function that requires two arguments
[6]:
def takes_two(a, b):
print(f"I received {a} and {b}")
# We can't just give it a list
inputs = [1, 2]
takes_two(inputs)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[6], line 7
4 # We can't just give it a list
5 inputs = [1, 2]
----> 7 takes_two(inputs)
TypeError: takes_two() missing 1 required positional argument: 'b'
[7]:
# But if we unpack our list, it works!
takes_two(*inputs)
I received 1, and 2
The function we called had no idea we ever unpacked anything.
takes_two(inputs) —> takes_two([1, 2])
takes_two(*inputs) —> takes_two(1, 2)
This can be done on the receiving end as well. Put a * in front of a function’s param
[25]:
def absorb_everything(*everything):
print(f"'everything' is a tuple. Its value is", everything)
absorb_everything() # It's optional!
absorb_everything(1)
absorb_everything(1, 2)
absorb_everything(1, 2, [3, 4])
'everything' is a tuple. Its value is ()
'everything' is a tuple. Its value is (1,)
'everything' is a tuple. Its value is (1, 2)
'everything' is a tuple. Its value is (1, 2, [3, 4])
It even works when we’re receiving things from a function
[26]:
def gives_four():
return 1, 2, 3, 4
# This won't work. We're assigning 4 things to 2 variables
one, two = gives_four()
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[26], line 5
2 return 1, 2, 3, 4
4 # It's giving us four things, so this will not work
----> 5 one, two = gives_four()
ValueError: too many values to unpack (expected 2)
Instead, we can put the first value in one, and everything else in the_rest
[14]:
one, *the_rest = gives_four()
print(one)
print(the_rest)
1
[2, 3, 4]
What about keyword arguments?#
In the “absorb_everything” example earlier, we lied to you. The term, *everything did not absorb everything. It absorbed all positional arguments.
[18]:
def absorb_everything(*everything, another_thing=None):
print("'everything': ", everything)
print("'another_thing': ", another_thing)
absorb_everything(1, 2, 3, 4)
'everything': (1, 2, 3, 4)
'another_thing': None
[19]:
absorb_everything(1, 2, 3, 4, another_thing='Fried Chicken')
'everything': (1, 2, 3, 4)
'another_thing': Fried Chicken
No problem! We can do the exact same thing with keyword arguments as we did with positional arguments.
Python has decided we must use ** instead of *, and it will return a dictionary of key-value pairs, instead of a tuple
[23]:
def actually_absorb_everything(*positional_args, **keyword_args):
print("Positional args tuple: ", positional_args)
print("Keyword args dictionary: ", keyword_args)
print()
actually_absorb_everything(1)
actually_absorb_everything(stuff='abcd')
actually_absorb_everything(1, 2, one=10, two=20)
Positional args tuple: (1,)
Keyword args dictionary: {}
Positional args tuple: ()
Keyword args dictionary: {'stuff': 'abcd'}
Positional args tuple: (1, 2)
Keyword args dictionary: {'one': 10, 'two': 20}
Keyword argument unpacking in particular is very powerful. You can unpack a dictionary into keyword arguments, the same way we unpacked a list into integers at the beginning of this tutorial
This is slightly less intuitive, because the string keys in your dictionary will be executed as real python keywords as soon as they’re unpacked.
In other words, **{'name': 'Jeff', 'age': 85} will immediately be treated as name='Jeff', age=85 inplace. No more strings.
[31]:
def takes_kwargs(size=None, fill_color=None, auto_open=None):
print("size: ", size)
print("fill_color: ", fill_color)
print("auto_open: ", auto_open)
options = {
'fill_color': 'blue',
'auto_open': True,
}
takes_kwargs(**options)
size: None
fill_color: blue
auto_open: True