Haskell World logo Haskell WorldWrite code, build worlds
Open Source

Haskell PVP Versioning Explained for Library Maintainers

You release version 1.4.2 of your library on a Friday, and by Monday three downstream packages fail to build because you removed one helper function you assumed nobody used.

Cabal file showing dependency version bounds next to a Hackage package page

You release version 1.4.2 of your library on a Friday, and by Monday three downstream packages fail to build because you removed one helper function you assumed nobody used. On Hackage, that kind of weekend is exactly what the Haskell PVP, the Package Versioning Policy, exists to prevent. It tells everyone who depends on you what a new version number actually promises.

This guide covers the rules that matter in daily maintenance, how to write bounds that follow them, and where the community still argues. I have maintained small libraries for years, and the mistakes below are ones I made first.

Why Haskell PVP Uses Four Numbers Instead of Three

A PVP version looks like A.B.C.D. The first two numbers together form the major version. That surprises people coming from SemVer, where only the first number is major. In the PVP, going from 2.1 to 2.2 is as serious as going from 2 to 3.

  • A.B is the major version. Bump it when you break anything.
  • C is the minor version. Bump it when you only add things.
  • D and anything after it is a patch level. Use it for fixes that do not touch the public interface.

Why two numbers for major? It lets a project signal a rewrite (bumping A) separately from a routine break (bumping B).

What Counts as a Breaking Change

The PVP defines breakage by the exported interface, not by intention. If code that compiled against your previous version could fail to compile against the new one, it is a major change. The common triggers are concrete:

  • Removing or renaming any exported function, type, constructor or module.
  • Changing the type signature of an export, even to something "more general" that breaks type inference downstream.
  • Removing a class instance, or changing the context of one.
  • Adding a method to a class without a default, so every user instance stops compiling.

Behaviour changes are a grey area. If a function keeps its type but now returns results in a different order, the PVP does not strictly force a major bump. I bump anyway when the change could silently corrupt someone's output, because a compile error is kinder than a wrong answer in production.

Additions are not free either

Adding a new export or a new instance only requires a minor bump. It still breaks some users: anyone importing your module unqualified without an import list can hit a name clash with their own function. The PVP handles this by asking those users to bound more tightly, which leads straight into how bounds work.

Writing Bounds in a Cabal File

Your build-depends section is where the PVP becomes practical. The modern shorthand is the caret operator. A caret bound on text 2.0 means at least 2.0 and below 2.1, which is exactly the range the PVP says is safe if you import qualified or with explicit lists.

How you importSafe upper boundExample for a dependency at 2.0.3
Qualified or explicit import listNext majorbelow 2.1
Open, unqualified importNext minorbelow 2.0.4
Orphan instances on its typesNext minor, test every releasebelow 2.0.4

The second row is why I now write explicit import lists for anything outside base. It turns a tight, annoying bound into a relaxed one and costs maybe ten extra lines per module. If you already fight solver errors, the walkthrough on fixing the cabal dependency resolution error pairs well with this section.

The Upper Bounds Debate, and Why "Just Drop Them" Is Bad Advice

Every few months someone posts that upper bounds cause more pain than they prevent, and that maintainers should only list lower bounds. The argument sounds reasonable: a new GHC ships, base bumps its major version, and hundreds of packages refuse to build even though most of them would compile fine.

I disagree with dropping them, for one reason. Without an upper bound, the solver will happily pick a dependency version you never tested, and when it fails the error lands on your users, not on you. A bound that is too tight produces a clear message saying no plan was found. A missing bound produces a type error deep inside your library that a beginner cannot read.

The real fix for bound friction is not deleting them. It is relaxing them quickly, which Hackage makes cheap.

Hackage Revisions Make Fixing Bounds Painless

Hackage lets a maintainer edit the cabal metadata of an already published version without uploading new code. These are called revisions, and they exist mostly for bounds. When a dependency releases a new major version and your test suite passes against it, you can widen the bound on your existing release in about two minutes from the web interface.

  1. Build and test locally against the new dependency version with a temporary allow-newer setting in cabal.project.
  2. Open your package page on Hackage and choose the option to edit package information.
  3. Change only the upper bound, review the diff Hackage shows, and save.

Revisions cannot change your code, your module list or your version number. That restriction is a feature: users can trust that version 1.4.2 always contains the same source. Hackage trustees also use revisions to tighten bounds on broken releases, which is part of the shared maintenance work described in how the Haskell community maintains its packages.

Habits From SemVer That Mislead Haskell Maintainers

SemVer and the PVP look alike, and that similarity causes three recurring mistakes.

Treating 0.x as permission to break things

In SemVer, a 0.x version signals instability and anything goes. The PVP has no such exception. Going from 0.3.1 to 0.3.2 must not break the interface. If you want freedom to break, bump to 0.4.

Bumping only the first number

Releasing 2.0 after 1.3 when the break is small wastes the A number and confuses readers of your changelog. A move from 1.3 to 1.4 says the same thing to the solver.

Forgetting that deprecation is not removal

Marking a function deprecated with a pragma only produces a warning, so it is a minor change. Remove it later in a major release, and mention both steps in the changelog so users can plan.

A Release Checklist That Keeps You Honest

Before every upload I run through the same five questions. It takes under ten minutes and has saved me from at least four broken releases.

  1. Did I remove, rename or retype anything exported? Then bump A.B.
  2. Did I only add exports or instances? Then bump C.
  3. Is this a pure bug fix with an identical interface? Then bump D.
  4. Do my own lower bounds still build? Test once with the oldest versions the solver allows.
  5. Does the changelog name every breaking change in one plain sentence?

FAQ

Is the Haskell PVP mandatory for Hackage uploads?

No tool enforces it at upload time, but Hackage expects it, Stackage curators assume it, and users report violations as bugs. Treat it as mandatory in practice.

Should I set an upper bound on base?

Yes. Base tracks GHC releases, and a new major version of base can remove or change functions you use. Widen the bound with a revision once you have tested the new compiler.

What version should a brand new library start at?

Many maintainers start at 0.1.0.0, which is what cabal init suggests. Remember that 0.1 is still a full PVP major version, so the same rules apply from the very first release.

The Practical Takeaway

Pick one library you maintain and audit its last three releases against the rules above: check that each version bump matches what changed, add explicit import lists where you rely on open imports, and give every dependency an upper bound you can widen later with a revision. More open source maintenance notes live in the open source section.

SR
Sonja Reiter

Sonja Reiter contributed to three major open source projects and started writing documentation when she noticed most tooling guides were written by people who had never taught anyone to use them. She covers build tools, package ecosystems, and the invisible quality-of-life differences between development setups.

More posts by Sonja

More in Open Source

01doorganico 084goad 3hot8 4465456 after5internprogram ahandfulofblessings ahmadpc ahomcybersecurity allodialresources annodex antiquesandperiodcollectibles aotc asianworldkashmir beedlectrics bloggertemplatefree bluefeatheragency bodyweightsolution campaign08blog carfixadvisor catstreetcollective cbstvsports cnfanspurchasing committeeof500yearsofdignityandresistance covertopsdjs cs-kaiyuangame cuestacomunicaciontotal cxoutlet dancebetgame deskgearhub destinyvillastay detectmagazine di33 diamondsteelbuildings donmo dyanadropship emarsysfinance emimattl escapechan eskitarih evautoplay everythingweddings-orlando farmzone feedviewerapp foxstudiosscratch gamevui123 gleasongrade greetingsday hacklabasu iw-kaiyun jagoanproperti jilidskripsi joltbusinessfinance juegodepokergratis juegospucca kaeru-festival latestmehndidesigns lestanzediottaviano litemagazines localcommunityhero lustrsazan mariashebets menswheels newwebstechs1 nikkit ootheday oxigenvpn plurfitness prefetial probudgetcarpetcleaning psychicmrsmartin pyaten qvantor rejoignezlogis rexzbot riselayouts ruchin rwxqsgd5 sakaltime shibiaowang shop-bot skrtips strcoding tetapatrium topselller uneplanteartificielle vipertechcr webtech1223411 wo-kygaming yamaha-mekar yeknava yoybuysheets zenunwa yamaha-mekar jilidskripsi ankertoto ankertoto ankertoto ankertoto