Safeguard
Developer Tools•

How do you write a purl for a Go, Ruby, or Python package?

Worked package URL examples for pypi, gem and golang, including the platform qualifier and the subpath. Steps to check a purl before it goes in an SBOM.

Safeguard Security Team
6 min read

Write a purl as pkg:type/namespace/name@version, with type pypi, gem or golang. PyPI and RubyGems usually need just a name and version. Go uses the full module path as namespace plus name. Add qualifiers after a question mark and a subpath after a hash just when they change the identity.

The pattern

The Package URL specification defines the form scheme:type/namespace/name@version?qualifiers#subpath. Scheme is always pkg. Type and name are required. Namespace, version, qualifiers and subpath are optional. Qualifiers are key and value pairs joined with ampersands.

Three worked examples

These come from the specification's own examples.

EcosystemPurlWhat each part says
Pythonpkg:pypi/django@1.11.1Type pypi, name django, version 1.11.1
Rubypkg:gem/jruby-launcher@1.1.2?platform=javaType gem, name jruby-launcher, version 1.1.2, platform qualifier java
Gopkg:golang/google.golang.org/genproto#googleapis/api/annotationsType golang, namespace google.golang.org, name genproto, subpath into the module, no version

Notice the Ruby example. The platform qualifier says which variant of the gem is meant. A gem built for Java is not the same artifact as one built for another platform, so the qualifier carries identity.

In the Go example the module path supplies the namespace and name, and the subpath points to a package inside the module. The example has no version, which is legal but rarely what you want in an SBOM.

Steps to write one by hand

  1. Pick the type that matches the registry the code came from.
  2. Take the name exactly as the registry spells it.
  3. If the ecosystem uses a prefix, such as a Go module host path, put it in the namespace.
  4. Add the exact resolved version, not a range.
  5. Add qualifiers that change which artifact you mean, such as platform or architecture.
  6. Add a subpath just if you are pointing inside the package.
  7. Percent encode special characters. The specification refers to ECMA-427 for full syntax, so check it for edge cases.

Mistakes that break matching

Using a range as a version, for example a caret range. A vulnerability database matches exact versions, so the component will never match.

Choosing the wrong type, for example writing a Python package as generic. The component then has no ecosystem for advisories.

Dropping the namespace for Go modules. Two modules with the same last path element would collapse into one identity.

Putting a local path or a git URL where a registry coordinate belongs, with no note that it is unusual.

How to check a batch

Pull every purl from your SBOM, sort them, and scan for missing versions, ranges and odd types. A short script or a spreadsheet filter is enough. Then feed a sample to an advisory lookup and see that each resolves.

The specification says purls are used in CycloneDX and SPDX files and in databases such as OSV, so a clean purl is what lets those systems find your component.

A worked example

You inherit an SBOM where three components have hand written identifiers. Below is a made up cleanup session.

As writtenProblemCorrected idea
pkg:pypi/Django@^1.11Range instead of an exact versionpkg:pypi/django@1.11.1
pkg:gem/jruby-launcher@1.1.2Missing platform when the Java build is meantpkg:gem/jruby-launcher@1.1.2?platform=java
pkg:golang/genproto@1.0Missing the module host path in the namespacepkg:golang/google.golang.org/genproto with a real version

The specification's own examples show the second and third shapes. The first correction follows from the rule that a version should identify one release, which is what lets advisory databases match.

Namespaces in each ecosystem

Not every ecosystem uses a namespace. In the PyPI example there is none. In the Go example the module path supplies it. Maven, as the specification notes, uses the group identifier. The rule of thumb is to follow the ecosystem's own naming. When in doubt, read the type specific rules the specification links to.

Where qualifiers matter

Qualifiers narrow identity. The specification names arch, distro, repository_url and platform as examples. Use them when the same name and version could refer to different artifacts. Leave them out when they do not change identity, because extra qualifiers can prevent a match with a database that omits them.

Subpaths

A subpath points inside a package. The Go example uses one to point at a package within a module. In an SBOM you rarely need subpaths, since the unit of tracking is the module. Use one when a finding or a note refers to a specific path.

A validation routine

  1. Extract all identifiers from the SBOM into a list.
  2. Check that each starts with the scheme and has a type and a name.
  3. Flag versions that contain range characters or are empty.
  4. Group by type and look for typos such as misspelled ecosystem names.
  5. Sample ten entries and look them up in the relevant registry.
  6. Fix the generator or its configuration, not just the output.

If you generate identifiers by script, add unit tests with the three examples above and a few of your own. Tests catch regressions when someone refactors the code.

Encoding

The specification shows percent encoding in its examples: an at sign in an npm scope becomes %40, and a slash inside a qualifier value becomes %2F. For the full rules it points to ECMA-427 Clause 5. If you hand write identifiers with unusual characters, read that clause rather than guessing.

Why bother

The specification says purls are used in CycloneDX and SPDX files and in vulnerability databases such as OSV. A clean identifier is what lets your component match an advisory without a human reading names. A sloppy one produces either false negatives, where a vulnerable component is missed, or false positives, where an unrelated package is flagged.

Governance

Give someone ownership of identifier quality. A short guideline, a test suite and a monthly sample check keep the data healthy. Without them, small errors accumulate across thousands of components.

Where Safeguard fits

Safeguard scans SBOMs and dependencies. Upload a file to the free scan at gold.safeguard.sh/scan and check that components resolve.

Try it yourself

Click through it at your own pace, or open it full screen.

Click into a few components and note how direct and transitive packages are separated.

Quick check

Does a Go purl need a version?

The spec allows it to be omitted, but include one in an SBOM so advisories can match.

What does the platform qualifier do?

It narrows which variant of the package you mean, as in the Ruby example with java.

Can I use a range as the version in a purl?

Avoid it. A version should identify one release so that advisories can be matched exactly.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.