Narwhals w Pythonie: kod niezależny od backendu DataFrame (2026)
Narwhals pozwala pisać jeden kod DataFrame działający na pandas, Polars, PyArrow, Modin i cuDF. Pokazuję integrację z FastAPI, pułapki i praktyczne wzorce.
Narwhals to lekka biblioteka Pythona, która pozwala pisać kod operujący na DataFrame'ach niezależnie od backendu. Ten sam plik uruchomisz na pandas, Polars, PyArrow, Modin i cuDF, bez ani jednego if isinstance(...). W praktyce oznacza to, że autorzy bibliotek (i my, którzy budujemy usługi FastAPI z pipeline'ami danych) nie musimy już wybierać między szybkim Polarsem a wszechobecnym pandas. W tym przewodniku pokażę, jak Narwhals działa pod maską, kiedy warto go użyć, i jak wpiąć go w produkcyjny endpoint HTTP w 2026 roku.
Narwhals udostępnia podzbiór API Polars, który jest tłumaczony w locie na pandas, PyArrow, Modin, cuDF i Dask, bez kopiowania danych.
Biblioteka jest zero-dependency (nie wymaga żadnego z backendów zainstalowanego jako zależność) i celowo utrzymuje stabilne API dla autorów bibliotek downstream.
Model wykonania jest leniwy: zwraca LazyFrame, a wywołanie .collect().to_native() materializuje wynik z powrotem do oryginalnego typu.
W 2026 roku Narwhals jest zależnością m.in. Altair, scikit-learn, Plotly, tqdm i shiny. Jeśli używasz tych bibliotek, prawdopodobnie już masz go w środowisku.
W FastAPI Narwhals eliminuje osobne implementacje ścieżek transformacji dla różnych źródeł danych: jeden handler obsługuje wszystkie.
Czym jest Narwhals w Pythonie?
Narwhals to compatibility layer, cienka warstwa, która przyjmuje jako wejście dowolny DataFrame (pandas, Polars, PyArrow Table, Modin, cuDF, PySpark, Dask) i zwraca obiekt o stabilnym, wąskim API wzorowanym na Polars. Kiedy wywołujesz metodę na tym obiekcie, Narwhals tłumaczy wywołanie na natywną operację odpowiedniego backendu, bez kopiowania danych, bez konwersji typów, bez pośredniego formatu.
Przez ostatnie dwa lata biblioteki takie jak Altair czy komponenty w scikit-learn stały przed dylematem. Albo wymuszać na użytkowniku konkretny DataFrame (i przegrywać wojnę o adopcję), albo utrzymywać osobne ścieżki dla pandas i Polars (i podwajać powierzchnię testów). Narwhals rozwiązuje to formalnie: autor biblioteki pisze kod używający wyłącznie Narwhals, a użytkownik podaje cokolwiek chce. Filozofia jest bardzo backendowa, trochę jak pydantic dla walidacji: jeden interfejs, wiele backendów pod spodem.
Warto zaznaczyć, co Narwhals nie jest: to nie jest kolejny silnik obliczeń, nie próbuje być szybszy niż Polars, nie ma własnego formatu w pamięci. To translator. Cała inteligencja siedzi w bibliotekach docelowych. Narwhals dba tylko o to, żeby jedno .filter(nw.col("age") > 18) zadziałało w pandas jako df[df["age"] > 18] i w Polars jako pl.col("age") > 18.
Kiedy używać Narwhals zamiast pandas lub Polars
Trzy scenariusze, w których w mojej praktyce Narwhals się wyraźnie opłaca:
Piszesz bibliotekę, wtyczkę lub SDK, którą będą używać ludzie z różnymi preferencjami. Nie chcesz wymuszać zależności od pandas ani Polars, a Narwhals pozwala przyjąć obydwa i nie zajmować stanowiska.
Utrzymujesz serwis (np. FastAPI), który konsumuje dane z kilku źródeł: jeden endpoint dostaje Parquet z S3 (naturalnie PyArrow), drugi dostaje wynik zapytania SQL z SQLAlchemy (naturalnie pandas), trzeci dostaje Polars z pipeline'u ML. Zamiast pisać trzy transformery, piszesz jeden w Narwhals.
Migrujesz stopniowo z pandas na Polars i chcesz w międzyczasie mieć kod działający na obydwu. Narwhals to naturalny punkt pośredni, który później można zamienić na czysty Polars.
Szczerze mówiąc, w moim ostatnim projekcie zaczęliśmy właśnie od trzeciego scenariusza. Migracja z pandas na Polars szła zespołowi opornie, bo nie chcieli robić "big bangu" i psuć czterech notebooków naraz. Wpięcie Narwhals jako etapu pośredniego rozładowało tę dyskusję w jeden sprint.
Instalacja i pierwszy kod DataFrame
Narwhals ma zero zależności runtime'owych. Instalujesz go osobno od backendu, którego chcesz używać:
pip install narwhals pandas polars pyarrow
# lub z uv, który w 2026 jest de facto standardem:
uv pip install narwhals pandas polars pyarrow
Minimalny przykład, który przyjmuje dowolny DataFrame i zwraca go w oryginalnym typie po transformacji:
Ta sama funkcja przyjmie pandas.DataFrame, polars.DataFrame i pyarrow.Table, i zwróci obiekt tego samego typu, z którym została wywołana. Bez konwersji, bez kopiowania kolumn.
Warto tu zwrócić uwagę na parę rzeczy, które w praktyce oszczędzają godziny debugowania. Po pierwsze, nw.col("age") to ekspresja, nie kolumna, jest leniwa i oceniana dopiero przy materializacji. Po drugie, from_native ma parametr strict=, który kontroluje, czy niesparsowany obiekt (np. słownik listy list) ma rzucać wyjątek, czy być przepuszczony. W kodzie produkcyjnym zawsze ustawiam strict=True. Tak samo jak w pydantic wolę twardą walidację na brzegu.
Narwhals stable.v1 kontra main API
Autorzy Narwhals wyciągnęli lekcję z ekosystemu Python: biblioteki, na których stoją inne biblioteki, nie mogą zmieniać sygnatur co release. Dlatego Narwhals udostępnia dwa importy:
import narwhals as nw # rozwojowe API, może się zmienić
import narwhals.stable.v1 as nw # stabilne API, gwarancje wsteczne
Jeśli piszesz kod aplikacyjny do własnego zespołu i aktualizujesz zależności regularnie, możesz spokojnie używać narwhals as nw. Jeśli piszesz bibliotekę, którą inni będą importować, używaj narwhals.stable.v1 as nw i nie pozwól, żeby update Narwhals zepsuł ci użytkownikom builds.
W repozytorium narwhals-dev/narwhals zespół prowadzi jawnie dokument z listą metod objętych stabilnymi gwarancjami i ich kanonicznymi tłumaczeniami dla każdego backendu. Polecam raz przeczytać ze zrozumieniem, zanim zaczniesz mocno na Narwhals polegać. To coś, co bardzo dobrze znam z FastAPI: pydantic.v1 vs pydantic.v2, dokładnie ten sam wzorzec, dokładnie z tego samego powodu.
Integracja z FastAPI i pydantic
Tu robi się ciekawie z mojej perspektywy, bo pipeline'y danych rzadko żyją same, prawie zawsze są schowane za endpointem HTTP. Poniżej realny wzorzec z produkcji, uproszczony: endpoint /employees/aggregate, który przyjmuje ładunek JSON, zwraca zagregowane statystyki, a wewnątrz używa Narwhals, tak żeby ten sam handler mógł pod spodem operować na pandas (dla małych requestów) lub Polars (dla większych batchy).
from __future__ import annotations
from typing import Literal
import narwhals.stable.v1 as nw
from narwhals.typing import IntoFrameT
import pandas as pd
import polars as pl
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class EmployeeRow(BaseModel):
department: str = Field(min_length=1)
age: int = Field(ge=0, le=120)
salary: float = Field(ge=0)
class AggregateRequest(BaseModel):
rows: list[EmployeeRow]
backend: Literal["pandas", "polars"] = "polars"
def aggregate(df_native: IntoFrameT) -> IntoFrameT:
df = nw.from_native(df_native, eager_only=True)
return (
df.filter(nw.col("age") >= 18)
.group_by("department")
.agg(
nw.col("salary").mean().alias("avg_salary"),
nw.len().alias("headcount"),
)
.sort("avg_salary", descending=True)
.to_native()
)
@app.post("/employees/aggregate")
def aggregate_endpoint(req: AggregateRequest) -> list[dict]:
records = [r.model_dump() for r in req.rows]
if req.backend == "polars":
df = pl.DataFrame(records)
else:
df = pd.DataFrame(records)
result = aggregate(df)
if isinstance(result, pl.DataFrame):
return result.to_dicts()
return result.to_dict(orient="records")
Zwróć uwagę na trzy rzeczy. Po pierwsze, walidacja danych dzieje się na brzegu, w pydantic, zgodnie z zasadą parse, don't validate. Do momentu, gdy dane trafiają do aggregate, mamy pewność, że kolumny istnieją i typy są poprawne. Po drugie, eager_only=True mówi Narwhals, żeby nie zwracał LazyFrame'a; w kontekście request-response chcemy natychmiastowej materializacji, a nie odroczonego wykonania. Po trzecie, jedyna gałąź warunkowa dotyczy formatu wyjścia, nie logiki biznesowej. Kod transformacji ma jedną ścieżkę.
Podobny wzorzec omawiam bardziej szczegółowo w artykule o wdrażaniu modeli scikit-learn z FastAPI. W tym samym stylu można Narwhals wpiąć jako warstwę przygotowania cech przed modelem.
Wydajność, narzut i tryb leniwy
Powtarzam pytanie, które słyszę najczęściej: czy Narwhals nie spowalnia Polars? Krótko: praktycznie nie. Narwhals nie kopiuje danych, buduje strukturę ekspresji, którą tłumaczy na natywne wywołanie backendu. Narzut to zwykle mikrosekundy per operacja, niezależnie od rozmiaru DataFrame. Dla operacji na milionach wierszy jest to niemierzalne.
Kluczowa jest za to opcja leniwa. Kiedy wywołujesz nw.from_native(df).lazy(), otrzymujesz LazyFrame, i cała reszta twoich operacji buduje plan zapytania, który wykonuje się dopiero po wywołaniu .collect(). Na Polars daje to darmowo dostęp do optymalizatora zapytań (predicate pushdown, projection pushdown), na pandas efektywnie jest to no-op (pandas nie ma leniwej egzekucji), ale zapewnia zgodność API.
import narwhals.stable.v1 as nw
def transform_lazy(df_native):
df = nw.from_native(df_native).lazy()
plan = (
df.filter(nw.col("age") >= 18)
.with_columns(nw.col("salary").log().alias("log_salary"))
.group_by("department")
.agg(nw.col("log_salary").mean())
)
# nic się jeszcze nie wykonało
return plan.collect().to_native()
W Polars ten kod jest automatycznie optymalizowany. Pushdown filtra age >= 18 może się zdarzyć przed odczytem z Parquet, co oszczędza I/O. Na pandas dostajemy dokładnie to samo API, ale bez optymalizacji. To bardzo pragmatyczny model: piszesz raz, płacisz różnicę w wydajności zgodnie z możliwościami backendu.
Jeśli chcesz wypchnąć samo Polars głębiej, zajrzyj do przewodnika po Polars w Pythonie. Znajdziesz tam m.in. detale planera zapytań, których Narwhals sam nie odsłania.
Narwhals vs Ibis: kiedy który
To pytanie pada w każdej dyskusji, bo obie biblioteki obiecują "portable dataframes". Krótka odpowiedź: rozwiązują różne problemy.
Cecha
Narwhals
Ibis
Docelowy użytkownik
Autorzy bibliotek Pythona
Analityk / inżynier danych
API wzorowane na
Polars
Własna abstrakcja przypominająca dplyr
Backendy in-memory
pandas, Polars, PyArrow, Modin, cuDF
pandas, Polars, DuckDB
Backendy SQL
Brak
DuckDB, BigQuery, Snowflake, Postgres, +20
Zależności
Zero
Zależy od backendu (sqlglot i inne)
Model wykonania
Eager + lazy (delegowane do backendu)
Lazy z kompilacją do SQL lub DataFrame
Rozmiar API
Wąski, celowo minimalny
Szeroki, funkcje SQL i statystyczne
Krótko: Narwhals jest dla ciebie, jeśli piszesz kod, który ma bezproblemowo działać na cudzych DataFrame'ach w Pythonie. Ibis jest dla ciebie, jeśli piszesz analitykę, która ma bezproblemowo działać na cudzych bazach danych. Nie konkurują. W praktyce widzę pipeline'y, które używają obu: Ibis do ekstrakcji z hurtowni, Narwhals do transformacji in-memory.
Więcej o samym silniku DuckDB, którego Ibis często używa jako backend, znajdziesz w artykule o DuckDB w Pythonie.
Pułapki i ograniczenia w praktyce
Kilka rzeczy, na które warto zwrócić uwagę, zanim wpuścisz Narwhals na produkcję:
Nie wszystkie metody są portowalne
Semantyka niektórych operacji różni się między backendami tak bardzo, że Narwhals celowo ich nie udostępnia. Klasyczny przykład to merge_asof / join_asof: pandas i Polars mają odmienne implementacje z odmiennymi gwarancjami, PyArrow nie ma tego wcale. Narwhals nie próbuje udawać, że działa, tylko rzuca NotImplementedError. Wolę tę uczciwość niż ciche różnice zachowania (ten exact bug ugryzł mnie kiedyś w pipeline finansowym w 2024, więc mówię z doświadczenia).
Typy dat i stref czasowych
Reprezentacje datetime potrafią się różnić: pandas ma datetime64[ns], Polars ma Datetime z konfigurowalną precyzją, PyArrow ma timestamp. Narwhals ujednolica to na poziomie API, ale przy zapisie z powrotem możesz stracić precyzję sub-mikrosekundową, jeśli konwertujesz z Polars Datetime[ns] do pandas przed 2.0. W 2026 z pandas 3.0 i PyArrow-backed dtypes ten problem w praktyce zniknął, ale warto o nim wiedzieć.
Testy: testuj na kilku backendach
Jeśli piszesz kod produkcyjny w Narwhals, twoja macierz testów powinna obejmować co najmniej pandas i Polars. Parametryzacja pytest daje to za grosze:
import pandas as pd
import polars as pl
import pyarrow as pa
import pytest
@pytest.mark.parametrize("constructor", [
lambda d: pd.DataFrame(d),
lambda d: pl.DataFrame(d),
lambda d: pa.table(d),
])
def test_aggregate(constructor):
data = {"department": ["eng", "eng", "sales"], "age": [25, 40, 30], "salary": [100.0, 150.0, 80.0]}
result = aggregate(constructor(data))
df = nw.from_native(result).to_native()
# dalej sprawdzasz zawartość
W CI odpalam dodatkowo PyArrow. Złapało mi to już parę pułapek związanych z nullowymi wartościami, których pandas cicho traktuje jako NaN, a PyArrow jako prawdziwy null.
Nie wszystko wymaga Narwhals
Jeśli piszesz jednorazowy skrypt ETL, który zawsze będzie działał na Polars, pisz w Polars. Jeśli robisz notebook analityczny, w którym używasz zawsze pandas, pisz w pandas. Narwhals rozwiązuje konkretny problem autora biblioteki lub autora usługi z heterogenicznymi źródłami. Wprowadzenie warstwy abstrakcji tam, gdzie nie jest potrzebna, to niepotrzebny narzut poznawczy.
Wersjonowanie i kompatybilność
Zespół Narwhals bardzo poważnie traktuje semver, ale sam ekosystem Polars i pandas wciąż się zmienia. Przypnij wersje w pyproject.toml i uważaj przy dużych release'ach. W 2026 przejście na pandas 3.0 z PyArrow jako domyślnym backendem dtypes zmieniło zachowanie kilku edge case'ów w Narwhals; dokumentacja opisuje to jawnie w oficjalnej dokumentacji Narwhals. Zajrzyj też do wpisu o lazy execution na blogu Polars, żeby zrozumieć, co dokładnie dostajesz "za darmo" po stronie backendu.
Jeśli walidujesz same DataFrame'y schematami, warto to zestawić z podejściem opisanym w moim wcześniejszym artykule o walidacji DataFrame'ów z Panderą. Pandera i Narwhals dobrze się uzupełniają (Pandera na brzegu, Narwhals w środku).
Najczęściej zadawane pytania
Czy Narwhals jest szybszy niż pandas?
Nie. Narwhals nie wykonuje żadnych obliczeń, deleguje wszystko do backendu, na którym pracujesz. Jeśli podasz pandas, będzie tak szybki jak pandas. Jeśli podasz Polars, będzie tak szybki jak Polars. Narzut samej warstwy tłumaczącej jest w praktyce niemierzalny dla realnych DataFrame'ów.
Do czego służy Narwhals w Pythonie?
Do pisania kodu operującego na DataFrame'ach niezależnie od backendu. Jedno API pokrywa pandas, Polars, PyArrow, Modin i cuDF. Głównymi odbiorcami są autorzy bibliotek (Altair, scikit-learn, Plotly już go używają) i inżynierowie utrzymujący serwisy z heterogenicznymi źródłami danych.
Jak zainstalować Narwhals?
Wystarczy pip install narwhals lub uv pip install narwhals. Biblioteka nie ma żadnych zależności runtime, backendy (pandas, Polars, PyArrow) instalujesz osobno, w zależności od tego, co chcesz obsługiwać.
Czym Narwhals różni się od Ibis?
Narwhals to warstwa nad DataFrame'ami in-memory z API wzorowanym na Polars, celuje w autorów bibliotek Pythona. Ibis to warstwa nad silnikami SQL i DataFrame'ami z własnym API, celuje w analityków chcących pisać kod działający na hurtowniach danych. W pipeline'ach często używa się obu.
Czy Narwhals nadaje się do produkcji w 2026 roku?
Tak. Narwhals ma stabilne API (narwhals.stable.v1), jest zależnością szeroko używanych bibliotek (Altair, scikit-learn, Plotly), a zespół utrzymuje bardzo dobre pokrycie testami dla każdego wspieranego backendu. W usługach FastAPI używam go od dłuższego czasu bez incydentów.
Które metody Polars nie są dostępne w Narwhals?
Narwhals celowo wyklucza operacje o silnie różnej semantyce między backendami, np. join_asof z konkretnymi trybami, część zaawansowanych okien i funkcji temporalnych z pełnymi opcjami strefy czasowej. Pełną listę zawsze warto sprawdzić w oficjalnej dokumentacji, zmienia się z każdym release'em.
dbt Fusion (Rust) parsuje 30× szybciej niż silnik Pythonowy, a dbt Core v2.0 alpha zostaje na Apache 2.0. Praktyczny przewodnik po migracji, microbatch i cenniku State Reuse.
Pandera to biblioteka do walidacji DataFrame'ow w Pythonie. Przewodnik 2026: schematy jako kod, backend pandas i Polars, lazy validation i testy pytest z gotowymi przykladami produkcyjnymi.
Praktyczny przewodnik wdrażania modeli scikit-learn z FastAPI: async endpointy z Pydantic v2, wersjonowanie, tuning uvicorn/gunicorn i konteneryzacja z realnymi budżetami latencji sprawdzonymi na produkcji.