diff --git a/modules/misc/news/2025/12/2025-12-06_11-05-43.nix b/modules/misc/news/2025/12/2025-12-06_11-05-43.nix
new file mode 100644
index 000000000..913889808
--- /dev/null
+++ b/modules/misc/news/2025/12/2025-12-06_11-05-43.nix
@@ -0,0 +1,10 @@
+{
+ time = "2025-12-06T10:05:43+00:00";
+ condition = true;
+ message = ''
+ A new module is available: `programs.ty`
+
+ It allows to manage the configuration and package
+ of the Python language server `ty` by Astral.
+ '';
+}
diff --git a/modules/programs/ty.nix b/modules/programs/ty.nix
new file mode 100644
index 000000000..b278ef7ce
--- /dev/null
+++ b/modules/programs/ty.nix
@@ -0,0 +1,51 @@
+{
+ pkgs,
+ config,
+ lib,
+ ...
+}:
+let
+ inherit (lib)
+ mkEnableOption
+ mkPackageOption
+ mkOption
+ literalExpression
+ ;
+
+ tomlFormat = pkgs.formats.toml { };
+ cfg = config.programs.ty;
+in
+{
+ meta.maintainers = with lib.maintainers; [ mirkolenz ];
+
+ options.programs.ty = {
+ enable = mkEnableOption "ty";
+
+ package = mkPackageOption pkgs "ty" { nullable = true; };
+
+ settings = mkOption {
+ type = tomlFormat.type;
+ default = { };
+ example = literalExpression ''
+ {
+ rules.index-out-of-bounds = "ignore";
+ }
+ '';
+ description = ''
+ Configuration written to
+ {file}`$XDG_CONFIG_HOME/ty/ty.toml`.
+ See
+ and
+ for more information.
+ '';
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ home.packages = lib.mkIf (cfg.package != null) [ cfg.package ];
+
+ xdg.configFile."ty/ty.toml" = lib.mkIf (cfg.settings != { }) {
+ source = tomlFormat.generate "ty-config.toml" cfg.settings;
+ };
+ };
+}
diff --git a/tests/modules/programs/ty/default.nix b/tests/modules/programs/ty/default.nix
new file mode 100644
index 000000000..be700b677
--- /dev/null
+++ b/tests/modules/programs/ty/default.nix
@@ -0,0 +1,4 @@
+{
+ ty-example-settings = ./example-settings.nix;
+ ty-no-settings = ./no-settings.nix;
+}
diff --git a/tests/modules/programs/ty/example-settings.nix b/tests/modules/programs/ty/example-settings.nix
new file mode 100644
index 000000000..4d460eadb
--- /dev/null
+++ b/tests/modules/programs/ty/example-settings.nix
@@ -0,0 +1,25 @@
+{ pkgs, ... }:
+
+{
+ programs.ty = {
+ enable = true;
+ settings = {
+ rules.index-out-of-bounds = "ignore";
+ };
+ };
+
+ test.stubs.ty = { };
+
+ nmt.script =
+ let
+ expectedConfigPath = "home-files/.config/ty/ty.toml";
+ expectedConfigContent = pkgs.writeText "ty.config-custom.expected" ''
+ [rules]
+ index-out-of-bounds = "ignore"
+ '';
+ in
+ ''
+ assertFileExists "${expectedConfigPath}"
+ assertFileContent "${expectedConfigPath}" "${expectedConfigContent}"
+ '';
+}
diff --git a/tests/modules/programs/ty/no-settings.nix b/tests/modules/programs/ty/no-settings.nix
new file mode 100644
index 000000000..23bc04bf3
--- /dev/null
+++ b/tests/modules/programs/ty/no-settings.nix
@@ -0,0 +1,17 @@
+{ pkgs, ... }:
+
+{
+ programs.ty = {
+ enable = true;
+ };
+
+ test.stubs.ty = { };
+
+ nmt.script =
+ let
+ expectedConfigPath = "home-files/.config/ty/ty.toml";
+ in
+ ''
+ assertPathNotExists "${expectedConfigPath}"
+ '';
+}