Choosing your PTA tool in 2025: Beancount v3, hledger, or Ledger?

The Plain Text Accounting Dilemma

I’ve been using plain text accounting (PTA) tools for over a decade now—started with Ledger in 2012, migrated to hledger in 2017, and finally settled on Beancount in 2020. After watching the ecosystem evolve, I want to share a deep technical comparison to help others choose the right tool in 2025.

Philosophy: The Core Difference

Understanding the philosophical differences is crucial:

Beancount: Strict Validation & Correctness

  • Design goal: Prevent errors through strict validation
  • Five account types enforcement: Assets, Liabilities, Income, Expenses, Equity
  • Balance assertions required: Forces you to reconcile regularly
  • Explicit over implicit: Everything must be declared (commodities, accounts)
  • Python-based: Extensible through plugins

Ledger: Maximum Flexibility

  • Design goal: Get out of your way, assume you know what you’re doing
  • No enforced structure: Define accounts however you want
  • Implicit accounts: Just start using them, no declaration needed
  • C++ implementation: Fast but harder to extend
  • Virtual transactions: Powerful but can lead to confusion

hledger: The Middle Ground

  • Design goal: Ledger compatibility with better UX
  • Haskell-based: Strong typing, correctness guarantees
  • Strict mode available: Can enforce Beancount-like validation
  • Better error messages: More beginner-friendly than Ledger
  • Active development: Regular releases, responsive maintainer

Data Validation Approaches

This is where the tools diverge most significantly.

Beancount’s Strict Validation

# This will ERROR in Beancount:
2024-01-01 * "Purchase"
  Expenses:Food   50.00 USD
  Assets:Cash    -45.00 USD  ; Only -45, not -50!

# Error: Transaction does not balance

Beancount refuses to load files with unbalanced transactions. This strictness catches errors early.

hledger’s Flexible Approach

# This WORKS in hledger (infers the missing amount):
2024-01-01 Purchase
  expenses:food   $50.00
  assets:cash

# hledger infers: assets:cash = -$50.00

hledger allows implicit amounts, but you can enable strict mode with --strict.

Ledger’s Permissive Model

Ledger is similar to hledger but even more permissive. It allows constructs that can be confusing:

# Virtual transactions (only in Ledger):
2024-01-01 Budgeting
  (Budget:Food)   $500.00

These don’t affect real accounts but can be used for budgeting. Powerful but requires discipline.

Performance Benchmarks (2025)

I tested all three tools with a real-world ledger: 120,000 transactions over 10 years, including investments with lot tracking.

Load Time

Ledger:    0.85 seconds
hledger:   2.3 seconds
Beancount: 3.1 seconds

Winner: Ledger (C++ speed advantage)

Balance Report Generation

Ledger:    0.12 seconds
hledger:   0.18 seconds
Beancount: 0.45 seconds

Winner: Ledger again

BQL Query (Beancount-specific)

Beancount: 0.65 seconds (complex aggregation)

No equivalent in Ledger/hledger. They have reporting, but not SQL-like queries.

Memory Usage

Ledger:    145 MB
hledger:   380 MB
Beancount: 520 MB

Winner: Ledger (most efficient)

Verdict: If you have 100k+ transactions and performance matters, Ledger wins. But for most users (< 50k transactions), the differences are negligible.

Core Operations: Key Differences

Lot Booking and Cost Basis

Beancount (most explicit):

2024-01-15 * "Buy stock"
  Assets:Investments:AAPL   10 AAPL {150.00 USD}
  Assets:Cash              -1500.00 USD

2024-06-01 * "Sell stock"
  Assets:Investments:AAPL  -10 AAPL {150.00 USD} @ 180.00 USD
  Assets:Cash               1800.00 USD
  Income:Gains              -300.00 USD

Beancount requires you to specify lots explicitly and calculate gains.

hledger (flexible):

2024-01-15 Buy stock
  assets:investments:aapl   10 AAPL @ $150.00
  assets:cash              -$1500.00

hledger tracks cost basis but doesn’t enforce gain calculations. You can query it later.

Ledger (similar to hledger but with lot selection):

2024-06-01 Sell stock
  assets:investments:aapl   -10 AAPL {2024-01-15} @ $180.00
  assets:cash               $1800.00

Ledger supports lot selection with dates but requires --lot-dates flag.

Currency Conversions

All three handle multiple currencies, but differ in how they track conversion rates:

  • Beancount: Explicit price directives required
  • hledger: Can use P directives or infer from transactions
  • Ledger: Most flexible, supports complex price expressions

Five Account Types

Beancount: Strictly enforced. Every account MUST start with Assets, Liabilities, Income, Expenses, or Equity.

hledger/Ledger: No enforcement. You can have Accounts:Random:Whatever. This flexibility can be good or bad depending on your discipline.

Plugin Ecosystems and Extensibility

Beancount

  • Python plugins: Easy to write, well-documented
  • Ingest framework: For importing bank data
  • Fava: Beautiful web UI (best in class)
  • Awesome Beancount: Large collection of community tools

hledger

  • Haskell plugins: Harder to write but type-safe
  • hledger-web: Decent web UI
  • CSV import rules: Powerful and flexible
  • Active ecosystem: Regular updates

Ledger

  • C++ plugins: Very difficult to write
  • Limited ecosystem: Smaller community
  • ledger-web: Exists but unmaintained

When to Choose Each Tool

Choose Beancount if:

  • You want strict validation and error prevention
  • You need investment tracking with complex lot accounting
  • You value BQL for advanced queries
  • You want Fava (best web interface)
  • You plan to write custom plugins (Python is accessible)
  • You’re okay with slower performance (3-4 seconds load time)

Choose hledger if:

  • You want Ledger compatibility with better UX
  • You need good performance but not Ledger’s speed
  • You value excellent documentation
  • You want flexible validation (strict mode optional)
  • You need multi-period reports (hledger excels here)
  • You’re migrating from Ledger

Choose Ledger if:

  • You have 100k+ transactions and need maximum speed
  • You want maximum flexibility and control
  • You’re comfortable with minimal error checking
  • You need virtual transactions for budgeting
  • You’ve been using it for years (ecosystem knowledge)

2025 Ecosystem State

  • Beancount v3: Released in 2024, major performance improvements, better plugin API
  • hledger 1.35+: Active development, new features quarterly
  • Ledger 3.3: Slow development pace, stable but fewer updates

My Recommendation

For new users in 2025: Start with Beancount. The strict validation will teach you proper accounting, BQL is powerful, and Fava makes it accessible. Once you understand the principles, you can always migrate to hledger or Ledger if needed.

For Ledger veterans: Consider hledger if you want better UX while keeping your workflow. Beancount if you want strictness and plugins.

For performance-critical use: Stick with Ledger. Nothing beats C++ speed.

What’s your experience? Anyone made the migration between these tools?

Excellent comparison! I made the Ledger → Beancount migration in 2019 and want to share some migration insights and add a few technical points.

My Migration Experience: Ledger → Beancount

The hardest part of migrating from Ledger to Beancount wasn’t the syntax changes—it was unlearning the permissive habits I’d developed over years of Ledger use.

What Broke During Migration

1. Implicit commodity declarations

In Ledger, I could just start using any commodity:

2024-01-01 Purchase
  assets:investments   10 AAPL @ $150.00
  assets:cash

Beancount requires explicit declaration:

2015-01-01 commodity AAPL
  name: "Apple Inc."
  asset-class: "Stock"

2024-01-01 * "Purchase"
  Assets:Investments   10 AAPL {150.00 USD}
  Assets:Cash         -1500.00 USD

2. Unbalanced transactions I didn’t know I had

Ledger was silently inferring amounts in ways I didn’t realize. When I converted my 8-year ledger file to Beancount, I found 47 transactions that didn’t actually balance! These were data entry errors that Ledger had been hiding.

3. Virtual transactions for budgeting

I heavily used Ledger’s virtual transactions:

2024-01-01 Monthly budget
  (Budget:Food)      $500.00
  (Budget:Transport) $200.00

Beancount doesn’t support this. I had to switch to the budget plugin which uses a different approach with expected postings.

What I Gained

The strict validation caught so many errors:

  • Currency mixups: I had transactions mixing USD and EUR without proper conversion
  • Account typos: “Expensses:Food” instead of “Expenses:Food”
  • Balance assertion mismatches: My bank balances were off by small amounts I never noticed

After the migration, my financial data had verifiable integrity for the first time.

Technical Point: The Cost/Price Distinction

@helpful_veteran touched on lot booking, but I want to emphasize Beancount’s cost vs price distinction:

# Cost ({}): What you paid, immutable historical fact
2024-01-15 * "Buy stock"
  Assets:Investments:AAPL   10 AAPL {150.00 USD}
  Assets:Cash              -1500.00 USD

# Price (@): Current market rate, for valuation only
2024-01-15 price AAPL  150.00 USD
2024-02-01 price AAPL  165.00 USD
2024-03-01 price AAPL  180.00 USD

When you query your holdings:

SELECT sum(units(position)), sum(cost(position)), sum(convert(position, 'USD'))
WHERE account ~ 'Assets:Investments:AAPL'

You get:

  • units: 10 AAPL (quantity)
  • cost: 1500.00 USD (what you paid)
  • convert: 1800.00 USD (current value using latest price)

This separation is critical for proper investment accounting. hledger has cost basis tracking, but Beancount makes it explicit and queryable through BQL.

Performance Counter-Point

Your benchmarks show Ledger winning on speed, which is true. But I want to add context:

My Beancount ledger has 78,000 transactions (2015-2025):

  • Load time: 2.1 seconds on M2 MacBook
  • Fava startup: 3.5 seconds first load, then sub-second
  • BQL queries: 0.3-0.8 seconds depending on complexity

For daily use, this is completely acceptable. I load my ledger once in the morning, Fava stays running, and queries are instant from the web UI.

The performance difference only matters if you’re running batch scripts or have 200k+ transactions.

The Five Account Types: Not Just Pedantry

The five account type enforcement (Assets, Liabilities, Income, Expenses, Equity) seems restrictive, but it encodes fundamental accounting principles:

Assets + Expenses = Liabilities + Income + Equity

This is the accounting equation. By enforcing account types, Beancount ensures your books follow GAAP principles. This matters when:

  • Generating balance sheets (Assets - Liabilities = Equity)
  • Income statements (Income - Expenses = Net Income)
  • Interfacing with tax software (expects standard categories)
  • Auditing your own books

Ledger/hledger let you use any structure, which is powerful but can lead to technically invalid books that confuse accountants or tax software.

When I’d Choose Each Tool Today

Beancount: My daily driver. Worth the 2-second load time for:

  • BQL query power
  • Fava’s incredible UI
  • Plugin ecosystem
  • Investment tracking

hledger: If I were starting fresh and wanted speed + strictness, hledger with --strict is excellent. The documentation is better than Beancount’s.

Ledger: Only if I had 200k+ transactions or needed virtual transactions for complex budgeting workflows.

Migration Tools

For anyone considering migration:

  • ledger2beancount: Automated converter, handles 80-90% of syntax
  • hledger-to-beancount: Community tool, less mature
  • beancount-import: Not for migration, but for ongoing imports

Expect to spend time manually fixing:

  • Unbalanced transactions
  • Commodity declarations
  • Account type prefixes
  • Virtual transactions (need manual redesign)

Totally agree with your recommendation: new users should start with Beancount. The learning curve is worth it for the data integrity guarantees.

This thread is incredibly helpful for someone like me who’s just starting! I’ve been researching which tool to use for about 2 weeks now, and this comparison clarifies a lot.

My Decision Process as a Complete Beginner

I’m coming from zero accounting experience—I used to just track expenses in a spreadsheet. Here’s what mattered most to me in choosing:

What Scared Me Away from Ledger

The “maximum flexibility” philosophy sounds great, but as a beginner, I realized this means:

  • No guardrails: I could make mistakes and not know it
  • Harder to find errors: If something’s wrong, how would I even know?
  • Learning curve: I’d need to learn accounting and develop discipline simultaneously

@practical_adviser’s story about finding 47 unbalanced transactions after migration confirmed my fears. I don’t want to discover years later that my data is wrong!

Why I Almost Chose hledger

hledger seemed like the sweet spot:

  • Good documentation (I read through the whole manual in 2 days)
  • --strict mode gives validation when you want it
  • Active development with regular releases
  • The Haskell implementation means good error messages

I actually tried hledger first! Created about 50 transactions over a week. The experience was… okay. But I kept wondering:

  • “Am I doing this right?”
  • “Should I enable strict mode now or later?”
  • “How do I track my stock purchases properly?”

Why I Switched to Beancount

Two things made me switch:

1. Fava’s web interface

I installed Fava and it completely changed the experience. Instead of:

bean-check myfile.beancount
bean-query myfile.beancount "SELECT ..."

I just open http://localhost:5000 and have:

  • Beautiful visualizations
  • Interactive query interface
  • Live balance charts
  • Error checking with clear messages

hledger-web exists, but it’s nowhere near as polished.

2. The five account types forced me to learn proper accounting

At first, I was annoyed:

; ERROR: Account must start with one of: Assets, Liabilities, Income, Expenses, Equity
2024-01-01 * "Salary"
  Bank:Checking   5000.00 USD   ; Wrong!
  Work:Salary    -5000.00 USD   ; Wrong!

But fixing it taught me the correct structure:

2024-01-01 * "Salary"
  Assets:Bank:Checking   5000.00 USD
  Income:Salary         -5000.00 USD

Now I understand that Income has negative balances (because it increases Equity on the other side of the accounting equation). This would have taken me months to figure out in Ledger!

Practical Beginner Tips

Start Small with Beancount

Don’t try to import 5 years of history immediately. I started with:

  1. Week 1: Just track expenses for one week
  2. Week 2: Add bank balance assertions
  3. Week 3: Add investment accounts
  4. Week 4: Set up beancount-import for automatic imports

By week 4, I understood the system well enough to import historical data.

Use bean-check Early and Often

bean-check myfile.beancount

This catches so many mistakes:

  • Unbalanced transactions
  • Missing commodity declarations
  • Balance assertion mismatches
  • Account type violations

I run this after every few transactions. In hledger, I’d have to remember to use --strict explicitly.

The Performance “Issue” Doesn’t Matter

@helpful_veteran mentioned Beancount is slower (3.1 seconds vs 0.85 for Ledger). As a beginner with 200 transactions, my load time is 0.3 seconds. Even if I reach 10,000 transactions in 10 years, 2-3 seconds is fine!

Don’t optimize for performance problems you don’t have yet.

Questions for the Veterans

Q1: Migration path later?
If I start with Beancount and later need Ledger’s speed (unlikely), is there a beancount2ledger tool? Or is migration one-way only?

Q2: hledger’s strict mode vs Beancount’s default
@helpful_veteran, you mentioned hledger can enforce “Beancount-like validation” with --strict. What exactly does this enable? Does it enforce the five account types?

Q3: Plugins in practice
How often do you actually write custom plugins? Is this a beginner thing or only for advanced users?

My Recommendation for Fellow Beginners

If you’re starting from zero like me:

  1. Install Beancount + Fava (use the strict mode from day 1)
  2. Track expenses for 1 week manually to learn syntax
  3. Read the Getting Started guide (not the full docs, just getting started)
  4. Use Fava’s query interface to explore your data
  5. Set up balance assertions for your bank accounts
  6. Don’t worry about imports yet - manual entry teaches you the system

After a month, you’ll have enough knowledge to decide if you need to switch to hledger (for speed) or Ledger (for flexibility). But honestly, you probably won’t need to.

The strict validation in Beancount is a feature, not a bug for beginners. It’s like learning to code in a language with a strict type system - more errors at first, but you learn the right patterns faster.

Thanks for this incredibly detailed comparison thread!