# Awpy parses Counter-Strike 2 demos in Rust and hands Python users Polars frames

> Awpy is a Rust parser for Counter-Strike 2 demo files with a Python API that returns Polars DataFrames, built on the shared pbdems2 core. It suits analysts who want round, kill and tick level tables rather than a match viewer.

**pnxenopoulos/awpy** — Python library to parse, analyze and visualize Counter-Strike 2 data

- Repository: https://github.com/pnxenopoulos/awpy
- Website: http://www.awpycs.com
- Stars: 623 · Forks: 85
- Language: Rust
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/pnxenopoulos-awpy

## A demo parser with three faces: Python, Rust and a CLI

Awpy parses Counter-Strike 2 demo files in Rust, and its Python API returns Polars DataFrames. That sentence contains the whole design decision: the heavy lifting is Rust, the interface most analysts want is Python, and the data model is Polars rather than pandas.

The repository description calls it a Python library to parse, analyze and visualize Counter-Strike 2 data, and the primary language of the repository is Rust. Both are true, and the split is visible in the workspace. The Cargo.toml lists four member crates, crates/awpy, crates/awpy-dev, crates/awpy-proto and crates/awpy-python, plus a scripts/build-protos directory that is excluded from the workspace.

Three surfaces are offered. The Python API with the Demo class, a Rust library with the Parser type, and a command line interface. The README states you can use the Python API, the Rust library, or the command line interface, and the reasons listed under Why Awpy are about reading match events, player state, rounds and statistics as DataFrames, selecting the datasets, ticks or entity properties you need, reconstructing rounds from game state including demos without round events, and using whichever of the three interfaces fits.

The homepage is awpycs.com, the documentation is on readthedocs, and there is a Discord server linked from the README.

## pbdems2 is the shared core, and Boon is the other consumer

The architecture has a deliberate seam. Awpy and the Deadlock parser Boon both use the shared pbdems2 core, and Awpy adds the Counter-Strike 2 protobufs, field decoders and name tables on top of it. There is a PBDEMS2 format guide in the pbdems2 documentation for the shared format.

That is a good structure for a parsing project, because the hardest part is rarely the game specific vocabulary, it is the container and framing. Keeping that in one crate and putting the per-game decoding in another means a fix to the shared format lands for both consumers, and it means the Deadlock parser does not have to reimplement what Awpy already knows about the file format.

The Cargo.toml pins pbdems2 at 0.4.1, and the workspace version is 3.1.0 with a rust-version of 1.88.0. The protobuf work uses prost at 0.14, with serde and serde_json for serialisation, and the workspace lints are strict: unsafe operations inside unsafe functions are denied, clippy correctness is denied while suspicious, style, complexity and perf are warnings, undocumented unsafe blocks are denied, and broken intra-doc links are denied in rustdoc.

For a library that decodes untrusted binary files, denying undocumented unsafe blocks is a meaningful line to draw. It does not prove the parser is correct, but it does mean the unsafe surface has to be justified in writing.

## Installing the Python package, the CLI and the Rust crate

The Python install is a single command, and the README recommends uv.

```bash
uv add awpy
```

Or pip.

```bash
pip install awpy
```

Python 3.11 or later is required, Awpy requires Polars 2.x, and releases include wheels for Python 3.11 through 3.15 on Linux, macOS and Windows. Installing a wheel does not require a Rust compiler, which matters because a pure Rust project otherwise pushes a toolchain requirement onto every Python user.

Installing the Python package also installs the awpy command, which handles demo inspection and map data management.

```bash
awpy info match.dem
awpy kills match.dem --limit 20
awpy stats match.dem --json
awpy get              # download the latest map-data release
```

The awpy get command downloads the latest map-data release, which is the piece that keeps the parser current with new maps without a code change.

For Rust users, the library is added from the repository as a git dependency.

```toml
[dependencies]
awpy = { git = "https://github.com/pnxenopoulos/awpy" }
```

There is also a Rust developer binary for parser internals debugging, awpy-dev, installed with cargo install --path crates/awpy-dev.

## Datasets are properties, and the table tells you what you get

The Python API is built around properties that return cached DataFrames, and the datasets are documented in a table.

The rounds dataset gives round timings, winners and end reasons from game state. kills covers kills, participants, positions and trade flags. damages covers damage events, participants and health and armor changes. The bomb dataset records bomb actions such as pickup, drop, plant start and finish, and defuse, with the acting player, position and bombsite.

Then there are the datasets that are really the point of a demo parser. grenades gives per-tick thrown grenade trajectories for smoke, HE, flashbang, molotov and decoy. fires has one row per burning inferno with position, thrower and a start and end tick range. smokes has one row per deployed smoke cloud with position, thrower and a tick range. shots records every weapon_fire event with the shooter's state and the active weapon state including clip, inaccuracy and scoped. blinds has one row per flash event with the thrower, the blinded player, both resolved, and the blind duration. And item_events records weapon item transactions such as purchase, pickup and drop.

The caching is the point. Accessing demo.kills twice does not parse twice, and demo.load accepts several dataset names to collect compatible datasets in one pass, which is the difference between a fast script and a slow one when you are pulling five tables for a batch of demos.

## Per-tick state versus a sampled view of the match

Two APIs give you player state, and the distinction matters for how you build a feature set.

The sampled call returns the game state at a moment, or sampled across the match. The first call asks for every player's state at one tick, the second asks for one sample per 64 ticks.

```python
snap = demo.snapshots(ticks=29000)             # every player's state at one tick
series = demo.snapshots(every=64)       # one sample per 64 ticks
```

Ticks gives per-tick player state, one row per player per tick, decoded in parallel. It returns a default set of fields, and you can name the fields you want instead.

```python
ticks = demo.ticks()                    # default: X, Y, Z, health, armor, team_num
ticks = demo.ticks(["health", "m_iTeamNum"])
```

So the sampled call returns a subset view and the per-tick call returns the full per-tick table, and the difference is the size of the result. A per-tick, per-player table over a full match is large, which is why being able to select fields is not a convenience but a necessity.

The other part of the API is events. Any game event is available in long form, and demo.events.names lists them.

```python
pings = demo.events.player_ping
```

That last line is the extension point. When you need a field that is not in a curated dataset, you pull the raw event and shape it yourself, which is how most analyst workflows end up going once the standard tables run out.

## Multipart demos have rules you have to follow

Demos that were split during recording are a common and awkward case, and Awpy has an explicit model for them.

You pass the files in recording order, and strings and Path objects can be mixed.

```python
demo = Demo(["overpass-p1.dem", "overpass-p2.dem"])
print(demo.stats)
state = demo.snapshots(part=1, ticks=5333)
```

The rules the README states are specific. Multipart tables keep each file's ticks and add a zero-based part column. The stats and players tables combine Steam IDs. Explicit tick queries require part, which is why the call above has to name part=1. And Awpy checks map, build, tick rate, round continuity and recording boundaries.

Those checks are the useful part. A set of files that came from a different match, or a different patch, will be caught rather than silently merged into nonsense.

One limitation is stated plainly: splits during live play are not supported. The multipart guide in the documentation is the place to read before you build a pipeline over multi-file demos, because the semantics of part and the combined Steam ID behaviour will shape your code.

The CLI mirrors the same surface, with a per-round table, kills with players resolved, and events with a summary option or a name to dump one event.

## The Rust API, and what the build profile says about priorities

The Rust side exposes a Parser that can be built from a file, with the file header, the event stream and a run to end callback over entities.

```rust
use std::path::Path;
use awpy::Parser;

let parser = Parser::from_file(Path::new("match.dem"))?;
println!("map: {:?}", parser.file_header()?.map_name);
```

The entity loop walks tick by tick and lets you filter on active state and class name, which is how you get at the pawns.

```rust
parser.run_to_end(|ctx| {
    for (_, entity) in ctx.entities.iter() {
        if entity.active && entity.class_name == "CCSPlayerPawn" {
            // read entity fields
        }
    }
})?;
```

The build configuration in Cargo.toml is unusually revealing about what the author considers expensive. The dev profile sets incremental to false, with a comment explaining that awpy-proto is a large generated crate and incremental bookkeeping balloons the target directory for little gain. The awpy-proto profile sets debug to false so you never step into prost generated proto code and skip its debuginfo. The dev profile for awpy and pbdems2 sets opt-level to 1 to optimise replay decoding even in debug builds. And the release profile turns on link time optimization with codegen-units set to 1, commented as better optimization and slower compile.

That is a project that cares about build times and binary size, which matters when the crate ships wheels to Python users on three operating systems.

## Three packages released on the same day, and where the churn is

Maintenance is active and the versioning is unusual enough to look at closely. The recent releases are awpy-v3.1.0 and awpy-python-v3.1.0, both dated 2026-10-17, and awpy-proto at 0.1.11076591+2000924 on the same day. The repository is not archived and the last push was on 2026-10-17.

So the Rust crate, the Python package and the generated protobuf crate are released separately, and the protobuf crate carries a build-stamped version rather than a clean semantic version. That is the natural consequence of generating protobufs from the game, since the generated code has to track the game, but it means the three artefacts do not move in lockstep and a Python user can end up on a Python package that expects a proto crate layout the current one has moved past.

The maintenance model for users is therefore version pinning, and reading the changelog before each bump.

The licence is MIT, with the text in the LICENSE file. The repo also has a deny.toml, which is the cargo-deny configuration used to check dependency licences and advisories, and a scripts/ directory alongside the crates. There is a readthedocs configuration in the repository, and the CI workflow badge points at a ci.yml file, so tests run on every change.

## Conclusion

Adopt Awpy if you are doing quantitative analysis on Counter-Strike 2 demos and want tidy Polars DataFrames for rounds, kills, damages and per-tick state, and you are comfortable pinning a fast moving Rust and Python API. Do not adopt it as a match viewer, a spectator overlay, or a live data source, since it works from recorded demo files rather than a running game. Before you build a pipeline on it, check the multipart guide for how splits are handled, confirm your demos come from a single recording rather than a split session, and pin the version because the Rust and Python packages are released separately and move quickly.

## FAQ

### How do I install Awpy?

Use uv add awpy or pip install awpy. Python 3.11 or later is required, Awpy requires Polars 2.x, and releases include wheels for Python 3.11 to 3.15 on Linux, macOS and Windows, so installing a wheel does not need a Rust compiler.

### What data does Awpy return from a demo?

Datasets include rounds, kills, damages, bomb, grenades, fires, smokes, shots, blinds and item_events, each returned as a Polars DataFrame. Dataset properties are cached, and demo.load can collect several compatible datasets in one pass.

### Does Awpy handle demos that were split during recording?

Yes, with rules. Pass the files in recording order, multipart tables keep each file's ticks and add a zero-based part column, stats and players combine Steam IDs, explicit tick queries require part, and Awpy checks map, build, tick rate, round continuity and recording boundaries. Splits during live play are not supported.

### Can I use Awpy as a Rust library?

Yes. The Rust crate is added from GitHub with awpy = { git = "https://github.com/pnxenopoulos/awpy" }, and the Parser type offers the file header, the event stream and a run to end callback over entities. The Rust developer binary awpy-dev is installed with cargo install --path crates/awpy-dev.

### What is the awpy command line tool used for?

Installing the Python package also installs the awpy command for demo inspection and map data management, with subcommands such as info, kills, stats, rounds, events and get, where get downloads the latest map-data release.

## Sources

- [License: MIT](https://github.com/pnxenopoulos/awpy/blob/main/LICENSE)
- [pnxenopoulos/awpy on GitHub](https://github.com/pnxenopoulos/awpy)
- [Project website](http://www.awpycs.com)
- [README](https://github.com/pnxenopoulos/awpy/blob/main/README.md)
- [Releases](https://github.com/pnxenopoulos/awpy/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/pnxenopoulos-awpy
