Awpy parses Counter-Strike 2 demos in Rust and hands Python users Polars frames
Python library to parse, analyze and visualize Counter-Strike 2 data
At a glance
- What is it?
- 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.
- Who is it for?
- 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.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
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.
uv add awpyOr pip.
pip install awpyPython 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.
awpy info match.dem
awpy kills match.dem --limit 20
awpy stats match.dem --json
awpy get # download the latest map-data releaseThe 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.
[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.
snap = demo.snapshots(ticks=29000) # every player's state at one tick
series = demo.snapshots(every=64) # one sample per 64 ticksTicks 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.
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.
pings = demo.events.player_pingThat 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.
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.
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.
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.
Editorial 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.
Frequently asked questions
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.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/pnxenopoulos-awpy)Was this useful?