Pkl 0.33 Release Notes

Pkl 0.33 was released on November 5th, 2026.
The latest bugfix release is 0.33.0. (All Versions)

The next release (0.34) is scheduled for February 2027. To see what’s coming in the future, follow the Pkl Roadmap.

Please send feedback and questions to GitHub Discussions, or submit an issue on GitHub.

Pkl is hosted on GitHub. To get started, follow Installation.

Highlights 💖

Generic Methods

Support for generic methods has been greatly expanded (#1849, #1940).

A generic method is a method that accepts types as arguments. This is a powerful tool that allows writing business logic that works over a variety of types.

For example, this code declares two parameters: type parameter T, and value parameter arg.

function identity<T>(arg: T): T = arg

Generic methods currently already exist in a limited sense:

  1. Only the standard library can declare generic methods.

  2. It is not possible to pass in type arguments when calling generic methods.

In Pkl 0.33, both of these restrictions are lifted.

To pass type arguments into a generic method, add the type surrounded by angle brackets:

result = identity<String>("Hello, World!")

If type arguments are omitted entirely, it behaves as calling the method with the unknown type. The following two are logically identical:

result = identity("Hello, World!")
result2 = identity<unknown>("Hello, World!")

This is a breaking change: in some cases, code that parses as a comparison expression now parses as a method call.

To read more about generic methods, consult the language reference.

To learn more about the design of generic method calls, consult SPICE-0032.

New standard library module: pkl:net

A new standard library module is introduced called pkl:net, for working with network types (#1834).

Its centerpiece is Url, a URL as defined by RFC 3986. A string is parsed with Url(), which accepts an absolute URL as well as a relative reference, and which throws if the string cannot be parsed:

import "pkl:net"

local url = net.Url("https://user:pw@example.com:8080/a/b c?q=1&q=2#f")

scheme = url.scheme                   // "https"
rawUserInfo = url.rawUserInfo         // "user:pw"
authority = url.authority             // "user:pw@example.com:8080"
rawHost = url.rawHost                 // "example.com"
rawPort = url.rawPort                 // "8080"
port = url.port                       // 8080
rawPath = url.rawPath                 // "/a/b%20c"
path = url.path                       // "/a/b c"
pathSegments = url.pathSegments       // List("a", "b c")
rawQuery = url.rawQuery               // "q=1&q=2"
queryParameters = url.queryParameters // Mapping { ["q"] = Listing { "1"; "2" } }
rawFragment = url.rawFragment         // "f"

Parsing is lenient: whatever cannot appear literally in a component is percent-encoded as its UTF-8 bytes, and whatever is already encoded is kept as written. Every component is held percent-encoded in its raw property, whether the input was encoded or not; the percent-decoded form of each is available as userInfo, host, path, query, and fragment. The derived pathSegments and queryParameters are percent-decoded too.

A URL can also be built from its components, or derived from another URL by amending it. A component given this way has to be percent-encoded already:

built = new net.Url {
  scheme = "https"
  rawHost = "example.com"
  rawPath = "/a%20b"
}.toString() // "https://example.com/a%20b"

local base = net.Url("https://example.com/a?q=1")

amended = (base) { rawQuery = null }.toString() // "https://example.com/a"

Each component is type-checked, so a URL built this way cannot be malformed. For example, a rawPort can only hold digits, only a URL with a rawHost can have one, and a rawPath of "/a b" is rejected. The decoded components are fixed, so they cannot be amended.

A relative reference can be resolved against an absolute URL, which also removes dot segments from the result’s path:

resolved = net.Url("https://example.com/a/b").resolve("../c").toString() // "https://example.com/c"

Comparing two URLs with == compares their components as written. To instead compare them as the syntax-based normalization of RFC 3986, where scheme and host are case-insensitive and percent-encoding is normalized, use equals():

same = net.Url("HTTPS://Example.com/%7Euser").equals(net.Url("https://example.com/~user")) // true

The module also provides standalone helpers: encodeUrlComponent() and decodeUrlComponent() for percent-encoding, and buildQuery() and parseQuery() for application/x-www-form-urlencoded queries.

Alongside this module, pkl:base gains String.isValidUrl, which tells whether a string is a valid absolute URL. It is stricter than net.Url(), which also parses a relative reference, and which percent-encodes whatever cannot appear literally in a URL component. The Uri typealias is now defined in terms of it, which is a breaking change.

New standard library module: pkl:syntax

A new standard library module is introduced called pkl:syntax, for working with Pkl source code (#1821, #1964, #1955).

It has three parts:

  • Parser parses a string or resource into a syntax tree, as either a module or an expression.

  • Node is a node of that tree. It has a type, children, the verbatim source text, and the source span. Nodes can be searched, folded, and replaced with the find*, fold, and replace* methods.

  • FormattingRenderer renders a tree back to Pkl source code, using the canonical Pkl formatter.

import "pkl:syntax"

local myModule = new syntax.Parser {}.parseModule("""
  class Point {
    x: Int
    y: Int
  }
  """)

className = myModule.findChild((n) -> n.type == "class").findChildOfType("identifier").text // "Point"

ifCount = myModule.fold(0, (acc, n) -> if (n.type == "if_expr") acc + 1 else acc) // 0

C Library (libpkl)

A new C library, libpkl, is introduced (#1238, #1799, #1801, #1805, #1812).

One of Pkl’s selling points is the ability to embed the language into another runtime as a library. However, currently, callers from non-JVM languages must spawn Pkl as a child process. This introduces a system dependency on downstream users, and also introduces complexity and overhead for the operating system.

To address this pain point, a new library called libpkl is released for the C language.

This is a low-level library meant to be called into from language binding libraries. It exposes the message passing API through a small C interface (pkl.h).

Archives containing the header, both static and dynamic (shared) libraries, and pkg-config files are published for each supported OS and architecture, as part of the GitHub release.

To read more about libpkl, consult the libpkl documentation.

To read more about the design of this feature, consult SPICE-0015.

Thank you to @KushalP for their contributions to this feature!

CPU Profiler

The Pkl CLI can now profile where an evaluation spends its CPU time (#1823, #1931).

Profiling is enabled by passing --profile-cpu-output, which sets the file to write the profile to. The profile is written in pprof format, so it can be explored with any tool that reads it, such as go tool pprof.

$ pkl eval --profile-cpu-output profile.pb.gz myConfig.pkl
$ go tool pprof -http=:8080 profile.pb.gz

Profiling is only exposed through the CLI. For more details, consult the CLI documentation.

Noteworthy 🎶

this as a type

The this keyword can now be used as a type (#1708, #1962).

A self type is the type of the object that a member is accessed on. Self types are useful when building class hierarchies, because they let a member refer to the same type as the object it belongs to, even after the class is extended.

Currently, Pkl has a self-type, module, but it specifically describes the type of the module object. There is no equivalent for regular classes nor objects.

To address this shortcoming, the this keyword is now also a type. It refers to the self-type of the directly enclosing object, class, or module.

When a class is extended, this in an inherited member refers to the subclass, not to the class that declares the member:

open class Node {
  parent: this?
  children: List<this>
}

class SpecialNode extends Node {
  special: Boolean
}

node: SpecialNode = new {
  special = true
  parent = new SpecialNode { special = false; children = List() } (1)
  children = List(new SpecialNode { special = true; children = List() })
}

bad: SpecialNode = new {
  special = true
  parent = new Node {} (2)
}
1 parent has type SpecialNode?.
2 Error: Node is not a SpecialNode.

The this type can be used in properties, method parameters and return types, function types, and type arguments. It cannot be used in a type alias body.

The this type has also been adopted in the standard library.

To read more about the design, see SPICE-0029.

Better new {} inference

The new {} expression can now correctly infer its parent in more positions (#1768, #1824, #1845, #1847, #1857).

An implicitly typed new {} expression tells Pkl to infer the parent when amending. When used inside a property, for example, it amends the class property default.

Using new {} in the following positions currently throw, and will work in Pkl 0.33:

  • Method arguments

  • Let bindings

class Bird {
  name: String
}

function greet(bird: Bird): String = "Hello, \(bird.name)"

greeting1 = greet(new { name = "Pigeon" }) (1)

greeting2 =
  let (bird: Bird = new { name = "Parrot" }) (2)
    greet(bird)
1 The argument is inferred to be a Bird.
2 The let binding is inferred to be a Bird.

Inference is not available where the parameter type is not known ahead of time. Additionally, it cannot be used for constructor methods like List(), generic methods like Pair(), and Function.apply().

It is an error for the parameter type to be this or module.

Support for parsing .netrc files into HTTP headers

Pkl now supports parsing .netrc files to configure HTTP Basic authentication headers (#1817, #1854).

A new method is introduced called netRcHeaders() on class pkl.EvaluatorSettings#Http.

It can be used to read credentials directly from a local .netrc file and mapped to HTTP Authorization headers:

~/.config/pkl/settings.pkl
amends "pkl:settings"

http {
  headers = netRcHeaders(read("/Users/johnappleseed/.netrc"))
}

It follows standard curl parsing rules, supporting comments, quoted values, and escaped characters.

Thank you to @ayashjorden for their contributions to this feature!

CLI Changes

Package installation to the local cache

The pkl project package command has gained a new --install flag that triggers installation of the built package(s) into the module cache (#1795).

Local cache installation is useful for local end-to-end testing of dependency trees. This functionality is also available via pkl-gradle.

Default file locations

For new setups, the CLI no longer stores anything under ~/.pkl (#1809).

It uses XDG-style locations on Unix and Known Folder locations on Windows:

Concern Unix (Linux/macOS) Windows Legacy fallback

Package cache

~/.cache/pkl

$LOCALAPPDATA/pkl/Cache

none

Settings file

~/.config/pkl/settings.pkl

$APPDATA/pkl/settings.pkl

~/.pkl/settings.pkl

CA certificates

~/.config/pkl/cacerts

$APPDATA/pkl/cacerts

~/.pkl/cacerts

REPL history

~/.local/state/pkl/repl-history

$LOCALAPPDATA/pkl/repl-history

none

On every OS, these locations can be overridden with XDG-style env vars. For example, setting XDG_CACHE_HOME will configure the cache directory.

Note that the existing ~/.pkl/cache directory is ignored, so Pkl will download packages to populate its cache if configured to do so.

To learn more about this design, consult SPICE-0031.

Thank you to @florinungur for their contributions to this feature!

Java Library Changes

pkl.net#Url maps to java.net.URI

The JVM bindings now understand the new pkl.net#Url class (#1834).

The Java and Kotlin code generators generate a java.net.URI for a property whose type is Url.

Two conversions are added to org.pkl.config.java.mapper.Conversions, both part of the preconfigured set:

  • pUrlToString — gives the same result as Url.toString()

  • pUrlToURI — throws ConversionException if the URL cannot be represented as a java.net.URI

Standard Library Changes

pkl:net

  • New module introduced.

pkl:syntax

  • New module introduced.

pkl:base

pkl:ref

pkl:protobuf

  • Module deprecated for removal in Pkl 0.34 (#1778).

Breaking Changes 💔

Removal of macOS amd64 (Intel) Binaries

Native pkl and pkldoc binaries for Intel Mac systems are no longer published with Pkl releases (#1769).

To continue running new Pkl releases on these systems, use an appropriate Java runtime and the jpkl and jpkldoc Java executables.

Type check changes for Class<T>

Currently, type arguments to the Class type are erased. Any Class value would typecheck against Class<T> for any value of T.

In Pkl 0.33, this erasure has been removed. A Class value typechecked against Class<T> must be a subclass of T (#1698).

If T does not resolve to a class type (i.e. it is a union type, nullable type, string literal type, parameterized type, or nothing), the type check will always fail.

Standard library properties marked const

Some properties within the pkl:math and pkl:release modules have been marked const (#1864).

Currently, module properties in pkl:math and pkl:release can be overridden by amending the module. Doing this makes little sense; these values are constants and overriding them by amending their module would be odd.

The following properties are now const and cannot be amended:

This is a breaking change for code that looks like this:

bogusMath = (import("pkl:math")) {
  minInt = -1
}

Changes to join method on List, Set, and Listing

The join method in List, Set, and Listing will now call the toString() method if overridden on an object (#1872).

This is a breaking change, but is also considered a bugfix.

The following snippet will produce different results:

class Bird {
  name: String

  function toString() = "{ My name is \(name) }"
}

hidden birds: Listing<Bird> = new {
  new { name = "Osprey" }
  new { name = "Eagle" }
}

res = birds.join("\n")

Currently, Pkl ignores the toString() override, and produces:

res = """
  new Bird { name = "Osprey" }
  new Bird { name = "Eagle" }
  """

In Pkl 0.33, the overridden toString() will be called, and produces:

res = """
  { My name is Osprey }
  { My name is Eagle }
  """

Uri validates its value

The Uri typealias in pkl:base is now String(isValidUrl) instead of a plain String (#1834).

A value typed as Uri must be a valid absolute URL according to RFC 3986. This means that a relative reference, or a URL containing a character that must be percent-encoded, no longer type-checks:

good: Uri = "https://example.com/some%20path"

bad1: Uri = "./some/path"                   // throws: not absolute
bad2: Uri = "https://example.com/some path" // throws: space must be percent-encoded

This affects every standard library member typed as Uri, among them Resource.uri, importGraph(), and the URI-valued properties of pkl:Project, pkl:EvaluatorSettings and pkl:DocPackageInfo.

As a consequence, an invalid URI is now reported as a type constraint violation at the point where it is set, rather than as an error raised later by whatever consumes it.

Changes to Reference construction

As part of the introduction of generic methods, the Reference() constructor method in pkl:ref takes the domain and referent types as type arguments instead of taking a Class parameter (#1849). This also makes it possible to construct references to non-class types, such as Listing<String> or String | Int.

The ref.Domain class has been renamed to ref.AbstractDomain.

import "pkl:ref"

class MyDomain extends ref.AbstractDomain { (1)
  function renderReference(reference: ref.Reference<MyDomain, Any>): String = "..."
}

// current
myRef = ref.Reference(new MyDomain {}, MyReferent, myData)

// new
myRef = ref.Reference<MyDomain, MyReferent>(new MyDomain {}, myData) (2)
1 In Pkl 0.32, extends ref.Domain.
2 Constraints in the referent type of a Reference type annotation are no longer an error, they are ignored.

Parser rule changes for comparison

To support generic method calls, the syntax for Pkl has changed to permit passing type arguments when calling a method.

This purposefully introduces an ambiguity to the grammar: the following expression can be parsed as either a generic method call, or as nested comparison expressions:

foo < Int > (5)

To resolve such ambiguities, such code is preferably parsed as a generic method call.

This contrived example is nonsensical today, because it is an error to compare a boolean. However, there are times when such code might be written in a well-formed way, for example:

res = foo(
  bar < baz,
  qux > (corge + 3)
)

In Pkl 0.33, this above snippet will interpret bar as a generic method call. To restore the old meaning, wrap the left-hand side with parentheses:

 res = foo(
-  bar < baz,
+  (bar) < baz,
   qux > (corge + 3)
 )

test.catch() and test.catchOrNull() force their result

The catch() and catchOrNull() methods now fully evaluate the value returned by the function they are given (#1910).

Currently, an error nested inside the returned value was not caught. It surfaced later, when the value was used. In Pkl 0.33, it is caught, and catch() returns its message.

Abstract methods must be implemented in extending non-abstract classes/modules

When extending an abstract class or module, it is now an error if any abstract methods are not implemented.

In the following snippet, class Swallow is invalid because it does not implement canCarry:

abstract class Bird {
  abstract function canCarry(fruit: Fruit): Boolean
}

class Swallow extends Bird {
  type: "European" | "African"
}

Removal of module type in const scopes

The module type should no longer be used inside typealiases, class bodies, annotation bodies, and const scopes. Such usages will emit a deprecation warning, and will become a full error in Pkl 0.34 (#1708).

The module type is a self-type, which refers to the this of the module. When a module is extended, the module type refers to the extending module. On the other hand, the definition of classes and typealiases cannot be changed by an extending module.

The following snippet is misleading, because the module property friend is a self-type, whereas class property Feature.animal is not.

Bird.pkl
open module Bird

friend: module? (1)

class Feature {
  animal: module? (2)
}
1 Self-type
2 Not a self-type

To have clearer semantics, the module type can no longer be used inside const scopes.

In most cases, such code should be replaced with a self-import.

Bird.pkl
open module Bird

import "Bird.pkl" (1)

class Feature {
  animal: Bird (2)
}
1 Self-import to use Bird as a type

Protobuf module deprecated

The pkl:protobuf module is deprecated without replacement (#1778).

The protobuf module renders Pkl data into the "textproto" format. However, Pkl is more expressive than protobuf, which means that Pkl’s renderer needs to make rendering choices that are bespoke to Pkl’s protobuf renderer.

For example, there is no native equivalent for Pkl’s null literal. To address this, Pkl’s protobuf renderer renders these as a "null" scalar.

These rendering choices are somewhat ad-hoc and likely to break from one usage to the next.

Users of protobuf are encouraged render Pkl into ProtoJSON instead.

Miscellaneous 🐸

  • The abstract modifier is now a no-op for properties. (#1781).

  • Abstract methods in non-abstract classes and modules are now rejected (#1647, #1780).

  • Enforce that non-abstract classes implement all abstract methods from superclasses (#1785).

  • Introduce a new internal representation for type metadata (#1838).

  • Improve error message when amending a property with no value set (#1867).

  • Use EnumSet for token categories (#1803).

  • String.toInt() now supports all hexadecimal, octal, and binary formats (#1808).

  • Handle conditional pluralization in error message (#1848).

  • Avoid using Java Object.toString() to present language-facing string representations (#1850).

  • Unify internal construction of objects (#1873).

  • Use module cache directory configured via the CLI or Gradle while evaluating PklProject and settings.pkl files (#1900).

  • Typed values are rendered with more helpful type names in error messages and Any.toString() (#1412).

  • Add support for customizing spring boot @ConfigurationProperties in generated Java/Kotlin code (#1897).

  • Improve error message when instantiating a class that cannot be instantiated (#1916).

  • Improve error messages when a type alias fails to type check (#1914).

  • Add descriptions and groups to the tasks created by the pkl-gradle plugin (#190).

  • Pkldoc no longer truncates the names of modules that do not start with the package’s module name prefix (#1909).

  • Reject file: URIs that include an authority (#629).

  • Upgrade to GraalVM 25.2.4 (#1804).

  • Improve performance of reading const members (#1835).

  • Improve performance of Listing and Mapping type casts (#1837).

  • Improve performance of lexical frame resolution (#1842).

  • Improve performance of amending null parents (#1836).

  • Improve performance of annotations without object bodies (#1843).

  • Improve performance of for and when generators (#1844).

  • Improve performance of parent property and subscript access (#1853).

  • Improve performance of typechecking (#1871).

  • Improve performance of string literal union types in more cases (#1875).

  • Improve performance of iterating object members (#1879). Thank you to @odenix for their contributions to this feature!

  • Fix some partial evaluation issues in the GraalVM compiler (#1944).

Bug Fixes 🐜

  • Fix incorrect behavior when objects are partially forced (#1790).

  • Fix issues publishing pkl-gradle (#1777, #1807).

  • Fix resolution of the module type in typealias bodies (#1796).

  • Don’t fail when loading known-malformed CA certs (#1806).

  • Fix resolution of type names in typealias constraints (#1826).

  • Fix parsing of block comments at the end of a file (#1828).

  • Sanitize block comment endings in generated Kotlin doc comments (#1830).

  • Enforce max collection size when decoding pkl-binary data (#1831).

  • Correctly omit glob imports from reflected imports (#1816).

  • Escape characters that cannot appear literally in rendered YAML (#1846).

  • Fix invalid prefix when generating spring boot config (#1862).

  • Fix pkl run behaviors where (sub)commands amend pkl:Command without extending it (#1882).

  • Fix String.endsWith() for overlapping regex matches (#1880, #1884).

  • Fix leak of external reader processes in the language binding server (#1885).

  • Fix decoding of Regex values from pkl-binary data (#1895).

  • Fix pkl eval of dependency notation source modules when using multiple file output (#1942).

  • Report an error, instead of overflowing the stack, when parsing or formatting code that is nested too deeply (#1918, #1953).

  • Fix type check of the module type when it is defined in a parent member body (#1936).

  • Fix type check of subscript key types for Map and Mapping references (#1933).

  • Fix hashCode() and == inconsistency for negative zero in Duration and DataSize (#1932).

  • Fix hashCode() inconsistency with == for Listing, Duration, and DataSize (#1922).

  • Fix toString() and value of Duration and DataSize values that are too large to fit in a Long (#1935).

  • Fix iteration over Dynamic objects that have a negative key, such as new Dynamic { [-1] = 2 } (#1948).

  • Fix enforcement of const member access inside methods (#1945).

  • Fix parameterized type checks in type alias constraints (#1906).

  • Fix resolution of qualified type names (#1915).

  • Fix lookaround regexes in String.lastIndexOf() and String.replaceLast() (#1908).

  • Fix formatter instability (#1911).

  • Improve normalization of URIs (#1937, #1954).

  • Missing const checks which allow accessing non-const members (#1963).