Skip to content

Commit 6258ad6

Browse files
Add notebook walking through Python integrations (#3150)
Co-authored-by: José Valim <jose.valim@dashbit.co>
1 parent 397dfed commit 6258ad6

File tree

3 files changed

+373
-0
lines changed

3 files changed

+373
-0
lines changed

assets/public/images/python.svg

Lines changed: 1 addition & 0 deletions
Loading

lib/livebook/notebook/learn.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ defmodule Livebook.Notebook.Learn do
8383
cover_filename: "github-stars.png"
8484
}
8585
},
86+
%{
87+
path: Path.join(__DIR__, "learn/intro_to_python.livemd"),
88+
details: %{
89+
description: "Learn how to use Python in your Livebook notebooks.",
90+
cover_filename: "python.svg"
91+
}
92+
},
8693
%{
8794
ref: :kino_intro,
8895
path: Path.join(__DIR__, "learn/kino/intro_to_kino.livemd")
Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
<!-- livebook:{"default_language":"python"} -->
2+
3+
# Working with Python
4+
5+
```elixir
6+
Mix.install(
7+
[
8+
{:pythonx, "~> 0.4.9"},
9+
{:kino_pythonx, "~> 0.1.0"},
10+
{:kino_db, "~> 0.4.0"},
11+
{:adbc, ">= 0.0.0"}
12+
],
13+
config: [adbc: [drivers: [:duckdb]]]
14+
)
15+
```
16+
17+
```pyproject.toml
18+
[project]
19+
name = "project"
20+
version = "0.0.0"
21+
requires-python = "==3.13.*"
22+
dependencies = [
23+
"numpy==2.2.3",
24+
"matplotlib==3.10.1",
25+
"pandas==2.2.3",
26+
"polars==1.24.0",
27+
"altair==5.5.0",
28+
"plotly==6.0.0",
29+
"seaborn==0.13.2",
30+
"pillow==11.1.0",
31+
"pyarrow==23.0.0"
32+
]
33+
```
34+
35+
## Introduction
36+
37+
Besides Elixir, Livebook supports running Python code. Not only that, it allows you to mix Elixir and Python code seamlessly. In this notebook we will explore various ways in which you can use Python in your notebooks.
38+
39+
To enable Python in a new notebook, you just need to click <kbd>+ Python</kbd> button right below the setup cell. This does two things:
40+
41+
* adds `:pythonx` as an Elixir dependency, which embeds a fully fledged Python interpreter directly in Elixir
42+
* inserts a **pyproject.toml** cell, where you can configure all the Python packages you need, as detailed in the **uv** package manager [documentation](https://docs.astral.sh/uv/concepts/projects/dependencies/)
43+
44+
By explicitly listing the required dependencies, Livebook knows exactly what to install, making the environment easily reproducible, no global `pip` installs required!
45+
46+
Having done that, you can now insert Python cells and write typical Python code.
47+
48+
> Note that Python cells also offer intellisense such as module/function/variable completion and on-hover documentation, which you can test throughout the cells below.
49+
50+
## Visual representation
51+
52+
As you can see in the setup section above, this notebook already lists a number of popular Python packages as dependencies. Livebook provides rich rendering for a variety of object, such as charts and dataframes, which we are going to see in this section.
53+
54+
<!-- livebook:{"break_markdown":true} -->
55+
56+
### Polars
57+
58+
[Polars](https://pola.rs) dataframes.
59+
60+
```python
61+
import polars as pl
62+
import datetime as dt
63+
```
64+
65+
```python
66+
df = pl.DataFrame(
67+
{
68+
"name": ["Alice Archer", "Ben Brown", "Chloe Cooper", "Daniel Donovan"],
69+
"birthdate": [
70+
dt.date(1997, 1, 10),
71+
dt.date(1985, 2, 15),
72+
dt.date(1983, 3, 22),
73+
dt.date(1981, 4, 30),
74+
],
75+
"weight": [57.9, 72.5, 53.6, 83.1], # (kg)
76+
"height": [1.56, 1.77, 1.65, 1.75], # (m)
77+
}
78+
)
79+
80+
df
81+
```
82+
83+
### Pandas
84+
85+
[Pandas](https://pandas.pydata.org) dataframes.
86+
87+
```python
88+
import pandas as pd
89+
import datetime as dt
90+
```
91+
> Note: the cells with `import` may take a while the first time you run them. That's an expected Python behaviour when importing a large module tree. Python stores a cached module bytecode in disk, so even if you restart the notebook, subsequent imports of that module should be fast.
92+
93+
```python
94+
df = pd.DataFrame(
95+
{
96+
"name": ["Alice Archer", "Ben Brown", "Chloe Cooper", "Daniel Donovan"],
97+
"birthdate": [
98+
dt.date(1997, 1, 10),
99+
dt.date(1985, 2, 15),
100+
dt.date(1983, 3, 22),
101+
dt.date(1981, 4, 30),
102+
],
103+
"weight": [57.9, 72.5, 53.6, 83.1], # (kg)
104+
"height": [1.56, 1.77, 1.65, 1.75], # (m)
105+
}
106+
)
107+
108+
df
109+
```
110+
111+
### Matplotlib
112+
113+
[Matplotlib](https://matplotlib.org) plots.
114+
115+
```python
116+
import matplotlib.pyplot as plt
117+
```
118+
119+
```python
120+
plt.plot([1, 2], [1, 2])
121+
plt.gcf()
122+
```
123+
124+
```python
125+
import numpy as np
126+
```
127+
128+
```python
129+
x = np.linspace(0, 10, 100)
130+
y = np.sin(x)
131+
132+
plt.plot(x, y, 'b-', label='sine wave')
133+
plt.title('Simple Sine Wave')
134+
plt.xlabel('x')
135+
plt.ylabel('sin(x)')
136+
plt.legend()
137+
plt.grid(True)
138+
139+
plt.gcf()
140+
```
141+
142+
### Seaborn
143+
144+
[seaborn](https://seaborn.pydata.org) plots.
145+
146+
```python
147+
import seaborn as sns
148+
```
149+
150+
```python
151+
df = sns.load_dataset("penguins")
152+
153+
sns.pairplot(df, hue="species")
154+
```
155+
156+
### Vega-Altair
157+
158+
[Vega-Altair](https://altair-viz.github.io) charts.
159+
160+
```python
161+
import altair as alt
162+
import pandas as pd
163+
```
164+
165+
```python
166+
source = pd.DataFrame({
167+
"a": ["A", "B", "C", "D", "E", "F", "G", "H", "I"],
168+
"b": [28, 55, 43, 91, 81, 53, 19, 87, 52]
169+
})
170+
171+
alt.Chart(source).mark_bar().encode(x="a", y="b")
172+
```
173+
174+
### Plotly
175+
176+
[Plotly](https://plotly.com/python) graphs.
177+
178+
```python
179+
import plotly.express as px
180+
```
181+
182+
```python
183+
px.line(x=["a", "b", "c"], y=[1, 3, 2], title="Sample figure")
184+
```
185+
186+
```python
187+
import pandas as pd
188+
```
189+
190+
```python
191+
df = pd.DataFrame({"a": [1, 3, 2], "b": [3, 2, 1]})
192+
193+
px.bar(df)
194+
```
195+
196+
### Pillow
197+
198+
[Pillow](https://python-pillow.github.io) images.
199+
200+
```python
201+
from PIL import Image
202+
from urllib.request import urlopen
203+
```
204+
205+
```python
206+
url = "https://github.com/elixir-nx/nx/raw/v0.9/nx/nx.png"
207+
Image.open(urlopen(url))
208+
```
209+
210+
## Elixir interoperability
211+
212+
In Livebook you can mix Elixir and Python code together. For example, let's define a variable in an Elixir cell:
213+
214+
```elixir
215+
elixir_variable = 1
216+
```
217+
218+
We can access it under the same name in a Python cell:
219+
220+
```python
221+
elixir_variable
222+
```
223+
224+
> Hint: the language is shown in the bottom-right corner of each code cell. You can change the language by clicking the language icon. When inserting a new cell, you can also change the langauge by clicking on the dropdown icon.
225+
226+
<!-- livebook:{"break_markdown":true} -->
227+
228+
An important implication of the variable interoperability is that you can use `Kino` inputs to provide data for Python code.
229+
230+
```elixir
231+
range_input = Kino.Input.range("How cool is that?")
232+
```
233+
234+
```elixir
235+
number = Kino.Input.read(range_input)
236+
```
237+
238+
```python
239+
if number % 3 == 0 and number % 5 == 0:
240+
print("FizzBuzz")
241+
elif number % 3 == 0:
242+
print("Fizz")
243+
elif number % 5 == 0:
244+
print("Buzz")
245+
else:
246+
print(number)
247+
```
248+
249+
Note that as you change the slider value, the cell indicator in the bottom-right changes to *Stale*. The value change tracking propagates from Elixir all the way to the Python cell!
250+
251+
<!-- livebook:{"break_markdown":true} -->
252+
253+
One gotcha here is that sometimes the default encoding may not be what you'd expect. Specifically, this is the case with strings:
254+
255+
```elixir
256+
hello_from_elixir = "Hello!"
257+
```
258+
259+
```python
260+
hello_from_elixir
261+
```
262+
263+
Elixir does not distinguish strings and binaries as separate data types - strings are just binaries, with the expectation that they follow utf-8 encoding. When passed to Python, a string therefore becomes a `bytes` object. However, converting it to a Python string is as easy as decoding the bytes:
264+
265+
```python
266+
hello_from_elixir.decode("utf-8")
267+
```
268+
269+
## ADBC and data processing
270+
271+
Here we are going to have a look at another type of interoperability, by combining Elixir and Python libraries.
272+
273+
Elixir has a [adbc](https://hexdocs.pm/adbc/Adbc.html) package, with bindings for Arrow Database Connectivity (ADBC). ADBC provides a standard interface for querying databases and getting results in the Apache Arrow format.
274+
275+
In fact, the Livebook's "Database connection" smart cell, already uses ADBC for some of the databases. Let's connect to an in-memory DuckDB for the sake of this example:
276+
277+
<!-- livebook:{"attrs":"e30","chunks":null,"kind":"Elixir.KinoDB.ConnectionCell","livebook_object":"smart_cell"} -->
278+
279+
```elixir
280+
:ok = Adbc.download_driver!(:duckdb)
281+
{:ok, db} = Kino.start_child({Adbc.Database, driver: :duckdb})
282+
{:ok, conn} = Kino.start_child({Adbc.Connection, database: db})
283+
```
284+
285+
Now, we can query the database, but since there is no data, let's use `SELECT` with `VALUES`.
286+
287+
In particular, because we want to further process the query result in Python, let's use the `Adbc.Connection.py_query/3` function. This will give use a Python table object that can be efficiently consumed by Python code.
288+
289+
```elixir
290+
{:ok, py_table} =
291+
Adbc.Connection.py_query(
292+
conn,
293+
"""
294+
SELECT *
295+
FROM (VALUES
296+
(1, 'Alice', 'Engineering', 92000.00),
297+
(2, 'Bob', 'Marketing', 78000.00),
298+
(3, 'Carol', 'Engineering', 105000.00),
299+
(4, 'Dave', 'HR', 65000.00),
300+
(5, 'Eve', 'Marketing', 83000.00),
301+
(6, 'Frank', 'Engineering', 98000.00)
302+
) AS employees(id, name, department, salary)
303+
ORDER BY department, salary DESC
304+
""",
305+
[]
306+
)
307+
```
308+
309+
The object has type `pyarrow.Table` and can be easily converted into a dataframe using the `polars` library:
310+
311+
```python
312+
df = pl.from_arrow(py_table)
313+
df
314+
```
315+
316+
which we can operate on as usual:
317+
318+
```python
319+
summary = (
320+
df
321+
.group_by("department")
322+
.agg([
323+
pl.col("salary").cast(pl.Float64).mean().alias("avg_salary"),
324+
pl.col("salary").max().alias("max_salary"),
325+
pl.col("name").count().alias("headcount"),
326+
])
327+
.sort("avg_salary", descending=True)
328+
)
329+
330+
summary
331+
```
332+
333+
Finally, if we want to get the result back as Elixir data structure, we can call `Adbc.Result.from_py/1`.
334+
335+
```elixir
336+
{:ok, result} = Adbc.Result.from_py(summary)
337+
Adbc.Result.to_map(result)
338+
```
339+
340+
```elixir
341+
result |> Table.to_rows() |> Enum.to_list()
342+
```
343+
344+
## Distributed Python with FLAME
345+
346+
[FLAME](https://hexdocs.pm/flame/FLAME.html) makes it easy to dynamically start additional machines and run code on them. Our Python integration is fully interoperable with FLAME, allowing you to run distributed Python code and keep references to Python objects across machines.
347+
348+
Livebook comes with a **FLAME runner** smart cell for configuring and starting a runner pool, however for that to work, you need to run the given notebook using either Fly or Kubernetes runtime. You can click the runtime icon in the sidebar to explore further.
349+
350+
The rest of this section will assume you have either a Fly or k8s runtime setup. Once that's done, running computation on another machine is as simple as:
351+
352+
<!-- livebook:{"force_markdown":true} -->
353+
354+
```elixir
355+
result =
356+
FLAME.call(:runner, fn ->
357+
1 + 1
358+
end)
359+
```
360+
361+
## Wrapping up
362+
363+
As you can see, there are many levels of interoperability between Elixir and Python. In your notebooks, you can mix and match code from both languages. In turn, you get access to the extensive ecosystem of Python packages for enhancing your data workflows, while still being able to orchestrate it with Elixir.
364+
365+
Enjoy!

0 commit comments

Comments
 (0)