Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Easy Compression with numcodecs

Authors
Affiliations
University of Helsinki
European Centre for Medium-Range Weather Forecasts
University of Helsinki

In this short course, we use the simple numcodecs [1] API for compression. In numcodecs, compressors are called Codecs and only have two important methods: codec.encode(data) and codec.decode(encoded). numcodecs works great for the simple case of compressing and decompressing in-memory numpy arrays in one go, though it can also be used with chunked data.

This product includes software produced by UChicago Argonne, LLC under Contract No. DE-AC02-06CH11357 with the Department of Energy.

import numcodecs
import numpy as np

Configuration

Each codec is a class that inherits from numcodecs.abc.Codec. Codecs are configured upon instantiation using keyword arguments. For example, here we create an instance of the lossless Zlib compressor:

codec = numcodecs.Zlib(level=6)
codec
Zlib(level=6)

We can obtain the full configuration of the codec in JSON format using codec.get_config():

config = codec.get_config()
config
{'id': 'zlib', 'level': 6}

And then use numcodecs.get_codec(config) to recreate the codec from its configuration:

numcodecs.get_codec(config)
Zlib(level=6)

Compression

We use the codec.encode(data) function to encode the data:

data = np.linspace(-10, 10, 41)
data
array([-10. , -9.5, -9. , -8.5, -8. , -7.5, -7. , -6.5, -6. , -5.5, -5. , -4.5, -4. , -3.5, -3. , -2.5, -2. , -1.5, -1. , -0.5, 0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5, 6. , 6.5, 7. , 7.5, 8. , 8.5, 9. , 9.5, 10. ])
encoded = codec.encode(data)
encoded
b'x\x9c5\xcc;\x0e@\x00\x14D\xd1W(\x14\n\x11\x11\x11\x11\xbf}\xb0s\x96b\tJ\xa5\xe0\xb8\xcd)&\x99\x88\xa7y\x7f\x89\x89#\x07\xf6\xec\xd8\xb2a\xcd\x8a%\x0b\xe6\xcc\x982a\xf0\xda>O\x1e\xfc;\x16;/\xc6\xea\x8f)3\xe6,X\xb2b\xcd\x86-;\xf6\x1c8r\xe2\xbc\xde>E\x1c\x7f'

Decompression

We use the codec.decode(encoded) function to decode the data:

decoded = codec.decode(encoded)
decoded
b'\x00\x00\x00\x00\x00\x00$\xc0\x00\x00\x00\x00\x00\x00#\xc0\x00\x00\x00\x00\x00\x00"\xc0\x00\x00\x00\x00\x00\x00!\xc0\x00\x00\x00\x00\x00\x00 \xc0\x00\x00\x00\x00\x00\x00\x1e\xc0\x00\x00\x00\x00\x00\x00\x1c\xc0\x00\x00\x00\x00\x00\x00\x1a\xc0\x00\x00\x00\x00\x00\x00\x18\xc0\x00\x00\x00\x00\x00\x00\x16\xc0\x00\x00\x00\x00\x00\x00\x14\xc0\x00\x00\x00\x00\x00\x00\x12\xc0\x00\x00\x00\x00\x00\x00\x10\xc0\x00\x00\x00\x00\x00\x00\x0c\xc0\x00\x00\x00\x00\x00\x00\x08\xc0\x00\x00\x00\x00\x00\x00\x04\xc0\x00\x00\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\xf8\xbf\x00\x00\x00\x00\x00\x00\xf0\xbf\x00\x00\x00\x00\x00\x00\xe0\xbf\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0?\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf8?\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x04@\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x0c@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x12@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00\x00\x16@\x00\x00\x00\x00\x00\x00\x18@\x00\x00\x00\x00\x00\x00\x1a@\x00\x00\x00\x00\x00\x00\x1c@\x00\x00\x00\x00\x00\x00\x1e@\x00\x00\x00\x00\x00\x00 @\x00\x00\x00\x00\x00\x00!@\x00\x00\x00\x00\x00\x00"@\x00\x00\x00\x00\x00\x00#@\x00\x00\x00\x00\x00\x00$@'

Some codecs, like Zlib, need to know the type and shape of the original data to correctly decompress it. For these cases, it is also possible to decode directly into an existing array using codec.decode(encoded, out=decoded):

decoded = codec.decode(encoded, out=np.empty_like(data))
decoded
array([-10. , -9.5, -9. , -8.5, -8. , -7.5, -7. , -6.5, -6. , -5.5, -5. , -4.5, -4. , -3.5, -3. , -2.5, -2. , -1.5, -1. , -0.5, 0. , 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5, 6. , 6.5, 7. , 7.5, 8. , 8.5, 9. , 9.5, 10. ])

Building up more complex compressors

It is often useful to combine several codecs, e.g. to use one to transform the data and one to compress it. Meta-compressors are such flexible combinators, and some are provided in the numcodecs-combinators [2] package:

from numcodecs_combinators.stack import CodecStack

CodecStack(a, b, c) combines codecs a, b, and c into a linear stack. During encoding, the data is first encoded by a, then b, then c. During decoding, the encoded data is first decoded by c, then by b, then a. The CodecStack supports an arbitrary number of sub-codecs.

The CodecStack also provides two useful helper methods:

  • CodecStack.encode_decode(data) encodes and then decodes the data array in one go.

  • CodecStack.encode_decode_data_array(da) encodes and then decodes an xarray.DataArray and preserves all metadata. If the data is chunked, the sub-codecs are applied independently to each data chunk.

from numcodecs_combinators.framed import FramedCodecStack

The FramedCodecStack is a specialised CodecStack that remembers the type and shape of the data during encoding, encodes it alongside the encoded data, and then uses this information during decoding to provide the out parameter to each codec.decode. The FramedCodecStack is thus useful when trying to stack codecs that require this information to decode correctly, or to ensure that any codec produces its encoded data as a byte string.

from numcodecs_combinators.best import PickBestCodec

Finally, the PickBestCodec(a, b, c) combinator tries to encode the data with codecs a, b, or c, chooses the one with the greatest reduction in byte size, and outputs its encoded data alongside the index of the codec that was picked. During decoding, the picked sub-codec is used to decode the data. The PickBestCodec supports an arbitrary number of sub-codecs.

Portable scientific compressors with numcodecs-wasm

In this short course, we utilize the numcodecs-wasm [3] project, which provides several scientific compressors in a numcodecs-compatible form. The compressors are compiled to WebAssembly to ensure that compression and decompression can be performed reproducibly across any CPU machine [4]. Each compressor is published as its own Python package on PyPi, e.g. numcodecs-wasm-sz3 for the SZ3 compressor.

The complete list of currently supported compressors is:

While numcodecs-wasm provides reproducibility and makes it easy to quickly test out different compressors, it has reduced performance as compressors are run single-threaded in a WebAssembly engine and not as native code. Furthermore, the codecs run inside a sandboxed 32 bit memory space and can typically only compress arrays up to 1-2 GB in size. To compress larger data with numcodecs-wasm, please chunk the data using xarray and use CodecStack.encode_decode_data_array(da).

Footnotes
  1. Reichelt, T., Tyree, J., Klöwer, M., Dueben, P., Lawrence, B. N., Baker, A. H., Faghih-Naini, S., Hoefler, T., & Stier, P. (2026). ClimateBenchPress (v1.0): A Benchmark for Lossy Compression of Climate Data. EGUsphere [Preprint]. Available from: Reichelt et al. (2026).

References
  1. Reichelt, T., Tyree, J., Klöwer, M., Dueben, P., Lawrence, B. N., Baker, A. H., Faghih-Naini, S., Hoefler, T., & Stier, P. (2026). ClimateBenchPress (v1.0): A Benchmark for Lossy Compression of Climate Data. 10.5194/egusphere-2026-60