prettycsharp
An opinionated formatter for C#, written in Python, in the spirit of Prettier. It parses each file with tree-sitter and reprints the whole file in one consistent layout: braces, indentation, line breaks, blank lines and a few safe rewrites such as file-scoped namespaces, sorted usings and expression-bodied members. It needs no .NET SDK.
Before:
using System;
using Microsoft.Extensions.Logging;
namespace Shop.Orders {
public class OrderService {
static private readonly int MaxRetries=3;
public decimal Total(Order order) { return order.Lines.Where(l => l.IsTaxable).Sum(l => l.Amount); }
public void Ship(Order order) {
if (order == null) return;
if (order.IsPaid) { Send(order); } else { Hold(order); }
}
}
}
After:
using Microsoft.Extensions.Logging;
using System;
namespace Shop.Orders;
public class OrderService
{
private static readonly int MaxRetries = 3;
public decimal Total(Order order) =>
order.Lines
.Where(l => l.IsTaxable)
.Sum(l => l.Amount);
public void Ship(Order order)
{
if (order == null) return;
if (order.IsPaid)
{
Send(order);
}
else
{
Hold(order);
}
}
}
Install
Python 3.10 or later.
pip install prettycsharp
Use
prettycsharp format src/
prettycsharp check src/
prettycsharp diff Order.cs
format rewrites files in place, check exits 1 when any file would
change, and diff prints a unified diff. --write, --check and --diff
are aliases. Given one file and no command, the formatted file goes to
stdout; - formats stdin to stdout, with --stdin-filepath naming the
path whose settings apply.
A target can be a .cs file, a folder, a .csproj (the files it
compiles, after its Compile Include and Remove items) or a .sln or
.slnx (every C# project in it). Folders are searched for .cs files.
| Option | Meaning |
|---|---|
-o KEY=VALUE, --option |
Set one setting, over every settings file. Repeatable. |
--config FILE |
Use this settings file instead of searching for one. |
--no-ignore |
Do not skip what .gitignore and .prettycsharpignore list. |
--stdin-filepath PATH |
The path stdin stands for. |
Exit status: 0 when nothing needs to change, 1 when check or diff found
a file that would change, 2 when a file could not be read, parsed or
formatted, or a setting is wrong. A file that does not parse is reported
and left untouched.
From Python:
from prettycsharp import CSharpSyntaxError, FormattingError, Settings, format_details, format_source
formatted = format_source(source)
narrow = format_source(source, Settings().with_values({"max_line_length": 100}))
# format_details also says which lines were left as written (format_around).
result = format_details(source, Settings().with_values({"syntax_errors": "format_around"}))
print(result.text, result.kept_lines)
Settings
Settings come from, highest precedence first:
-ooptions on the command line;- the nearest
.prettycsharp.toml, orpyproject.tomlwith a[tool.prettycsharp]table, found by walking up from each file; - the standard keys of
.editorconfig(indent_style,indent_size,max_line_length,end_of_line,charset,insert_final_newline, and the Roslyn keys for braces,else/catch/finallyplacement, using order, namespace style, braces and modifier order); - the defaults.
# .prettycsharp.toml
max_line_length = 100
brace_style = "kr"
continuation_keywords = "same_line"
align_assignments = true
Every setting, its default, its alternatives and an example of each layout are in DESIGN.md.
Skipping files and code
bin/, obj/, hidden folders and generated files (*.g.cs,
*.Designer.cs, and files marked <auto-generated>) are always skipped.
So is whatever .gitignore lists inside a git repository, and whatever a
.prettycsharpignore file lists, in gitignore syntax.
Inside a file, // prettycsharp-ignore leaves the next statement or member
as written, and // prettycsharp-ignore-start ... // prettycsharp-ignore-end
leaves everything between them as written.
How it stays safe
Every output is parsed again and compared with the input as syntax trees, with only the changes the settings allow (parentheses, modifier and using order, attribute lists, trailing commas, expression bodies, namespace style, braces and regions) taken out of the comparison. The comments must hold the same words. If anything else differs, the file is not changed and the command reports the difference. String literals, including verbatim, raw and interpolated strings, are printed exactly as written.
Conditional compilation
#if branches can split a declaration, as in:
#if NET10_0_OR_GREATER
public sealed class Pool : IAsyncDisposable
#else
public sealed class Pool : IDisposable
#endif
{
Such a file is formatted the way the compiler reads it, one set of defined symbols at a time: the directive lines and the inactive branches are set aside, the rest is formatted, and they are put back as written. Symbol sets are chosen so that every branch is active in one of them, so every branch gets formatted. The result is checked under each symbol set, and a file where any branch would change in meaning is left untouched.
Code the parser cannot read
prettycsharp reads C# through tree-sitter's C# grammar, and syntax newer
than the grammar, such as C# 14's null-conditional assignment, does not
parse. By default such a file is reported with its line and column and left
untouched. With syntax_errors = "format_around", the formatter finds the
fewest lines it cannot read, leaves just those as written, formats the rest,
and names the lines it kept:
prettycsharp format src/ -o syntax_errors=format_around
This works together with conditional compilation: in a file split by #if,
each view is formatted around its own unreadable lines. A file is still left
untouched when hiding lines does not make it parse.
License
Metadata
Release files for prettycsharp 1.1.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| prettycsharp-1.1.4.tar.gz | 181.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| prettycsharp-1.1.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 258.6 kB
Release files / prettycsharp-1.1.4.tar.gz
| Download URL | prettycsharp-1.1.4.tar.gz |
|---|---|
| Size | 181.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
22741ef706fd0a2c0b12a4dbeb5dabb0173b26880e792aa32b9daef88e2057b0
|
|
BLAKE2b-256 checksum How to use checksums |
785c52df9f407e117181bcb7344d140e092be345a04c1dd3f227a49fb5a183a3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency logRelease files / prettycsharp-1.1.4-py3-none-any.whl
| Download URL | prettycsharp-1.1.4-py3-none-any.whl |
|---|---|
| Size | 77.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d7a8b250e8b7aa8a528e73e238721a8c9c91bf1b16643d633c95cf8228f258fd
|
|
BLAKE2b-256 checksum How to use checksums |
a359849623a01c05632aff43c7f0efb5e99c76a6ce583b259a891add7c924bad
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency log