Count lines of code in Python projects with comment-aware breakdowns
Project description
count-lines
Count lines of code across multiple languages with a comment-aware breakdown.
Supports Python, JavaScript (incl. JSX), TypeScript (incl. TSX), and Rust. Scans local folders or shallow-clones public git repositories on the fly.
Supported languages
| Language | Extensions |
|---|---|
| Python | .py |
| JavaScript | .js, .mjs, .cjs |
| JSX | .jsx |
| TypeScript | .ts |
| TSX | .tsx |
| Rust | .rs |
JSX files are parsed with the JavaScript grammar (the modern JS grammar handles JSX natively).
Installation
uv tool install python-count-lines
Run pcl -v to check the installed version.
Usage
pcl # current directory
pcl path/to/repo # a specific folder
pcl src/app.py # a single file
pcl github.com/psf/requests # a public repo (shorthand)
pcl https://github.com/psf/requests # full URL also works
Output
count-lines
Path: /home/me/projects/mixed-app
Folders 12
Source files 47
typescript 28
rust 12
python 7
Total lines 9,820
code 6,540
doc lines 1,180
comments 520
blank 1,580
Top 5 largest files
backend/src/lib.rs 1,420
web/src/components/App.tsx 984
backend/src/handlers.rs 910
web/src/utils/parser.ts 802
scripts/migrate.py 610
When --exclude is used, the headline rows (folders, files, total) carry a dim
delta showing how much was filtered out:
Folders 1 | -50% of 2
Source files 7 | -36% of 11
Total lines 427 | -26% of 580
Remote repositories
If the target looks like a git URL or a shorthand, pcl performs a shallow clone
(--depth 1 --filter=blob:none) into a temp directory, runs the scan, and cleans up.
git must be on PATH.
| Form | Example |
|---|---|
| HTTPS | https://github.com/psf/requests.git |
| SSH | git@github.com:psf/requests.git |
ssh:// |
ssh://git@github.com/psf/requests.git |
git:// |
git://github.com/psf/requests |
| Shorthand | github.com/psf/requests |
Shorthand is also recognised for gitlab.com, bitbucket.org, and codeberg.org.
Counting rules
Each supported source file is parsed once via tree-sitter; every line is classified into exactly one bucket:
| Bucket | What it is |
|---|---|
| blank | whitespace-only line |
| comment | line inside a non-doc comment (e.g. # in Python, // and /* */ in JS/TS/Rust) |
| doc | Python module/class/function docstrings, JSDoc /** */, Rust ///, //!, /** */, /*! */ |
| code | everything else |
Resolution priority on overlap: comment > doc > blank > code. So a blank line
inside a multi-line docstring counts as doc (it's part of the doc content),
while a trailing # ... on a code line stays code.
Tree-sitter parses each file; comment-only and string-literal nodes are mapped
to the right bucket per language. Strings containing // or # are never
mistaken for comments. On parse errors tree-sitter's error recovery still
surfaces well-formed regions.
Excludes
--exclude accepts one or more fnmatch
patterns. They combine as a logical OR — a path is excluded if it matches any pattern.
Each pattern is tested two ways:
- against the full path relative to the scan root (e.g.
src/migrations/*) - against every individual path component, including the filename
(e.g.
testsmatches anytests/folder;*_test.pymatches any matching file)
Hidden directories (starting with .) and __pycache__ are skipped by default.
Examples
# Folder names — match at any depth
pcl . --exclude tests
pcl . --exclude tests docs
# Anchor a folder pattern to a specific path
pcl . --exclude "src/migrations/*"
# Filename patterns
pcl . --exclude "test_*.py" # test_foo.py
pcl . --exclude "*_test.py" # fetch_orders_test.py
pcl . --exclude "*test*.py" # anything with 'test' in the name
# Combine freely — folders, paths, and filenames at once
pcl . --exclude tests docs "src/migrations/*" "*_test.py" "test_*.py"
Tips
- Quote glob patterns (
"*_test.py") so the shell doesn't expand them against your current directory beforepclsees them. --excludeis greedy (consumes all following words). Put the target before it, or separate with--:pcl /repo --exclude tests "*_test.py" # target first ✓ pcl --exclude tests "*_test.py" -- /repo # -- terminator ✓ pcl --exclude tests "*_test.py" /repo # /repo eaten ✗
Flags
| Flag | Description |
|---|---|
target |
folder, file, or git URL/shorthand. Defaults to . |
--exclude PATTERN [PATTERN ...] |
fnmatch patterns to skip |
--lang NAME [NAME ...] |
limit counting to the named languages (default: all supported) |
--strip-comments |
exclude comment-only lines from the headline LOC total |
-v, --version |
print the installed version and exit |
--strip-comments only changes the headline number; the breakdown is always shown.
It composes with everything else:
pcl github.com/psf/requests --exclude tests docs "*_test.py" --strip-comments
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file python_count_lines-0.1.1.tar.gz.
File metadata
- Download URL: python_count_lines-0.1.1.tar.gz
- Upload date:
- Size: 47.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f8bf6c553a9a3b622f6dca8d5148a312d1377eca0b2c819ef8d1ca04d934c3a6
|
|
| MD5 |
db6b7ab080b45f26e3b4fd1306957a81
|
|
| BLAKE2b-256 |
c9633c146e253f03c7365eb1a88551a137782c067039ff5caebef07e45ceff2b
|
Provenance
The following attestation bundles were made for python_count_lines-0.1.1.tar.gz:
Publisher:
publish.yml on frndvrgs/python-count-lines
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_count_lines-0.1.1.tar.gz -
Subject digest:
f8bf6c553a9a3b622f6dca8d5148a312d1377eca0b2c819ef8d1ca04d934c3a6 - Sigstore transparency entry: 1489034362
- Sigstore integration time:
-
Permalink:
frndvrgs/python-count-lines@60ca34ffc5183701019cf1bff00ea9b90ae8e2be -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/frndvrgs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@60ca34ffc5183701019cf1bff00ea9b90ae8e2be -
Trigger Event:
release
-
Statement type:
File details
Details for the file python_count_lines-0.1.1-py3-none-any.whl.
File metadata
- Download URL: python_count_lines-0.1.1-py3-none-any.whl
- Upload date:
- Size: 14.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c6711adb5c141cd7df56bb2ddc17bb9098ac37cfe154cc9f987105452ffc4aab
|
|
| MD5 |
09c000e87758fdf1d2e5cb5d2c665b7c
|
|
| BLAKE2b-256 |
b6f3907bbfe36ddd5b94785352ff1cbe179e9d2170d3bc8b2d3b63c1a5f9e22f
|
Provenance
The following attestation bundles were made for python_count_lines-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on frndvrgs/python-count-lines
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
python_count_lines-0.1.1-py3-none-any.whl -
Subject digest:
c6711adb5c141cd7df56bb2ddc17bb9098ac37cfe154cc9f987105452ffc4aab - Sigstore transparency entry: 1489035089
- Sigstore integration time:
-
Permalink:
frndvrgs/python-count-lines@60ca34ffc5183701019cf1bff00ea9b90ae8e2be -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/frndvrgs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@60ca34ffc5183701019cf1bff00ea9b90ae8e2be -
Trigger Event:
release
-
Statement type: