dask
Parallel computing library for Python built around lazy task graphs. Dask Array breaks NumPy-shaped work into chunks, Dask DataFrame partitions pandas-like tables, Dask Bag handles Python object streams, and delayed or futures express custom tasks. Graphs can run on local threads or processes, or through the separate distributed scheduler across a cluster. It helps scale familiar PyData operations, but it is not automatic acceleration and does not make algorithms with poor partitioning or high shuffle cost disappear.
Dask earns its complexity when work partitions naturally and genuinely exceeds one process or one machine. If pandas fits, or if your pipeline is dominated by shuffles and global state, the simpler tool will usually be faster and easier to operate.
Use it if
- Your NumPy or pandas-shaped workload is larger than memory but can be split into independent chunks or partitions
- You need one task graph to combine file reads, transformations, custom Python functions, and model work
- You want to prototype locally and later run the same graph on Dask Distributed with diagnostics and worker memory management
- You can inspect partitions, task graphs, shuffles, and scheduler dashboards instead of treating parallelism as a black box
- The data fits comfortably in memory and pandas or NumPy already completes quickly: Dask adds graph construction, scheduling, serialization, and partition overhead
- Your operation needs frequent global ordering, row-by-row dependencies, or repeated full-data shuffles: Dask DataFrame is strongest when partitions can work mostly independently
- You expect every pandas API to exist with identical behavior: Dask DataFrame is lazy and partitioned, requires metadata inference, and cannot cheaply know global divisions unless they are established
- You need straightforward distributed deployment without operating a scheduler and workers: the distributed extra, networking, environment parity, dashboard access, memory limits, and cluster lifecycle are your responsibility
- You cannot make tasks coarse enough to offset scheduler overhead: the Dask best-practices documentation warns that very large graphs and many tiny tasks create overhead before useful computation begins
Setup reality
A plain pip install dask installs the task-graph core and a small dependency set, not every collection and not the distributed scheduler. Use dask[array], dask[dataframe], dask[distributed], or dask[complete] deliberately; current DataFrame support requires pandas 2+ and PyArrow 16+, while version 2026.7.1 requires Python 3.10+. Local threaded execution often works immediately for NumPy, pandas, and compiled code that releases the GIL, but Python-heavy functions may need processes or distributed workers. Process-based code must be importable and should create its Client under if __name__ == '__main__' to avoid recursive worker startup. A cluster also needs the same code and package versions on every worker, reachable scheduler ports, configured temporary storage, and realistic memory limits. Lazy objects do nothing until compute, persist, or a distributed submission occurs. Calling compute repeatedly rebuilds work unless shared intermediates are persisted, while calling compute on a huge result can pull all partitions into the client and exhaust its memory. Chunk and partition size decide performance: too small creates enormous graphs and scheduler overhead; too large removes parallelism and causes worker spilling or death. DataFrame custom functions may need explicit meta because Dask's sample-based inference can run user code early or guess wrong. Shuffles, joins on unknown indexes, and repartitioning can transfer most of the dataset. The dashboard is not optional decoration when tuning real workloads; watch task stream, memory, spill, and worker balance before scaling out.
Patterns
Build a chunked Dask Arrayparallelize-array
import dask.array as da
x = da.from_array(numpy_array, chunks=(10_000, 1_000))
y = (x - x.mean(axis=0)) / x.std(axis=0)
result = y.compute()Choose chunks that are large enough to amortize scheduling but small enough for several chunks to fit in each worker's memory.
Read and aggregate Parquetread-partitioned-dataframe
import dask.dataframe as dd
df = dd.read_parquet('s3://bucket/events/*.parquet', columns=['user_id', 'amount'])
totals = df.groupby('user_id').amount.sum()
result = totals.compute()The final result is collected into client memory. Keep it lazy or write it out if the grouped result is still large.
Reduce data before a groupbyfilter-before-shuffle
recent = df[df.timestamp >= cutoff][['account_id', 'amount']]
summary = recent.groupby('account_id').amount.sum(split_out=16)
summary.to_parquet('s3://bucket/account-summary/', overwrite=True)Filter and select columns before groupby so less data crosses the network during the shuffle.
Run a function per DataFrame partitionmap-dataframe-partitions
import pandas as pd
meta = pd.DataFrame({'user_id': pd.Series(dtype='int64'), 'score': pd.Series(dtype='float64')})
def score_partition(pdf):
return pdf.assign(score=pdf.value / pdf.value.max())[['user_id', 'score']]
scored = df.map_partitions(score_partition, meta=meta)Provide meta when inference is uncertain; otherwise Dask may execute the function on sample data and infer incorrect dtypes.
Build a delayed task graphwrap-custom-functions
from dask import delayed
@delayed
def load(path):
return read_one_file(path)
@delayed
def combine(parts):
return merge(parts)
result = combine([load(path) for path in paths]).compute()Do not call delayed functions inside another delayed function when a normal Python call can represent the same coarse task.
Run a local distributed clusterstart-local-client
from dask.distributed import Client
if __name__ == '__main__':
with Client(n_workers=4, threads_per_worker=2, memory_limit='4GiB') as client:
print(client.dashboard_link)
result = computation.compute()Guard process startup in scripts. Install dask[distributed], and inspect the dashboard rather than assuming more workers help.
Submit dependent futuressubmit-futures
from dask.distributed import Client, as_completed
client = Client('tcp://scheduler.example:8786')
loaded = client.map(load_record, record_ids)
scored = [client.submit(score_record, item) for item in loaded]
for future in as_completed(scored):
store(future.result())future.result() transfers the value to the client. Store large outputs from workers or gather only compact results.
Keep a reused collection on workerspersist-shared-intermediate
filtered = client.persist(df[df.active])
by_country = filtered.groupby('country').amount.sum().compute()
by_plan = filtered.groupby('plan').amount.sum().compute()persist consumes cluster memory but prevents both downstream results from rereading and refiltering the source.
Write results without gatheringwrite-partitioned-output
cleaned.to_parquet(
's3://bucket/cleaned/',
partition_on=['event_date'],
write_index=False,
overwrite=True,
)overwrite is destructive at the target dataset path; use a new versioned path when replacement is not intended.
Target practical partition sizesrepartition-dataframe
df = dd.read_parquet(source)
df = df.repartition(partition_size='256MB')
df.to_parquet(destination, write_index=False)Repartitioning itself moves data and may be expensive. Measure existing partition sizes before adding this step.
Show local computation progressdiagnose-with-progress
from dask.diagnostics import ProgressBar
with ProgressBar():
result = computation.compute(scheduler='threads')ProgressBar is for local schedulers; distributed workloads should use the Client dashboard and task stream.
Choose worker spill storageconfigure-temporary-storage
import dask
dask.config.set({
'temporary-directory': '/mnt/local-ssd/dask',
'distributed.worker.memory.target': 0.60,
'distributed.worker.memory.spill': 0.70,
})Set configuration before creating workers. Slow or undersized temporary storage can turn spilling into a cluster-wide bottleneck.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| ray | PyPI | You need distributed actors, serving, and general Python application tasks more than NumPy or pandas compatibility |
| modin | PyPI | You mainly want a pandas-compatible front end and prefer the execution engine to stay less visible |
| pyspark | PyPI | You need a mature JVM-backed SQL and DataFrame platform for organization-scale clusters and data lakes |