task: metadata: install: pip install -e .[dev]
patch: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e65e54e2..2992a923b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,12 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](http://keepachangelog.com/)
 and this project adheres to [Semantic Versioning](http://semver.org/).
 
-## Unreleased
+
+## Unrealeased
+
+### Changed
+
+- App `title` and `sub_title` attributes can be set to any type https://github.com/Textualize/textual/issues/2521
 
 ### Fixed
 
diff --git a/src/textual/app.py b/src/textual/app.py
index 113a7a220..12f66cf04 100644
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -316,6 +316,7 @@ class App(Generic[ReturnType], DOMNode):
         the name of the app if it doesn't.
 
         Assign a new value to this attribute to change the title.
+        The new value is always converted to string.
         """
 
         self.sub_title = self.SUB_TITLE if self.SUB_TITLE is not None else ""
@@ -328,6 +329,7 @@ class App(Generic[ReturnType], DOMNode):
         the file being worker on.
 
         Assign a new value to this attribute to change the sub-title.
+        The new value is always converted to string.
         """
 
         self._logger = Logger(self._log)
@@ -406,6 +408,14 @@ class App(Generic[ReturnType], DOMNode):
         self.set_class(self.dark, "-dark-mode")
         self.set_class(not self.dark, "-light-mode")
 
+    def validate_title(self, title: Any) -> str:
+        """Make sure the title is set to a string."""
+        return str(title)
+
+    def validate_sub_title(self, sub_title: Any) -> str:
+        """Make sure the sub-title is set to a string."""
+        return str(sub_title)
+
     @property
     def workers(self) -> WorkerManager:
         """The [worker](guide/workers/) manager.

problem_statement: App title and sub_title should be converted to strings
I'm thinking that `App.title` and `App.sub_title` should be converted to strings.

Balancing convenience with hiding errors, this is probably the right thing to do 90% of the time.
repo: Textualize/textual
test_cmd: pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
test_patch: diff --git a/tests/test_app.py b/tests/test_app.py
index e529cfbd7..54bde8221 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -36,3 +36,33 @@ async def test_hover_update_styles():
         # We've hovered, so ensure the pseudoclass is present and background changed
         assert button.pseudo_classes == {"enabled", "hover"}
         assert button.styles.background != initial_background
+
+
+def test_setting_title():
+    app = MyApp()
+    app.title = None
+    assert app.title == "None"
+
+    app.title = ""
+    assert app.title == ""
+
+    app.title = 0.125
+    assert app.title == "0.125"
+
+    app.title = [True, False, 2]
+    assert app.title == "[True, False, 2]"
+
+
+def test_setting_sub_title():
+    app = MyApp()
+    app.sub_title = None
+    assert app.sub_title == "None"
+
+    app.sub_title = ""
+    assert app.sub_title == ""
+
+    app.sub_title = 0.125
+    assert app.sub_title == "0.125"
+
+    app.sub_title = [True, False, 2]
+    assert app.sub_title == "[True, False, 2]"