Repository: urwid/urwid (language: python)

Issue:
Drop Python 2 support
##### Description:
Python 2 reached its EOL December 31, 2019. I understand that ~30% of urwid users still on Python 2 at the moment (according to [PyPI stats](https://pypistats.org/packages/urwid)).

Advantages of dropping Python 2:
* decrease burden of development and maintenance;
* no more `six`, no more `str`/`bytes` errors;
* new language features supported (for example, `asyncio` module, type hints).

What's urwid's deprecation policy about old Python versions?


Discussion / hints:
It seems to me whatever norms / policies around deprecation may have existed previously for the project wouldn't necessarily apply to such an important, ecosystem-wide change as the EOL of Python 2.  It has taken a lot of blood, sweat, and tears to get folks to overcome their resistance to upgrading, and many would say that it's long overdue.

This makes me think that any attempts to continue supporting the EOLed version would be extremely counterproductive.  If someone wants to continue using Python 2.x despite the EOL, they can use older urwid versions.  If that prevents them from using some new functionality, it's not like they weren't warned for the past nine years (PEP 404 was written in 2011).  

I would support ripping the band-aid off now and dropping Python 2.x support ASAP.

See also #393. 
Py3 migration is indeed a well-beaten path, and a fertile topic for stories.

It's well about the time for Urwid to do it, too.

I've played several roles in this drama, been fighting on both sides; let me suggest a polite way to do it.

 1. Drop Py2 support from the **mainline branch**, `master` in our case — where all development happens, where PRs are getting merged into, where 2.1.x and later releases are cut from.
 2. **Keep** Py2 compat in a *life support* branch — 2.0.1 and later minor releases (2.0.2, 2.0.3 etc). No new features in this branch, not even "doc typo" PRs, no any changes if at all possible — the branch is reserved for critical fixes only.

For a good example, see [pytest](https://github.com/pytest-dev/pytest/branches) — their `4.6.x` branch keeps Py2 compat and minimum activity, while `5.3.x` and `master` have dropped py2 and moved along.

Has there been any progress on this?

Fix patch:
diff --git a/.travis.yml b/.travis.yml
index 74f091f96..17e452e6e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,19 +1,17 @@
 language: python
-dist: bionic
+#dist: bionic
 matrix:
   include:
-  - python: 2.7
-    env: TOXENV=py27
-  - python: 3.5
-    env: TOXENV=py35
-  - python: 3.6
-    env: TOXENV=py36
   - python: 3.7
     env: TOXENV=py37
   - python: 3.8
     env: TOXENV=py38
-  - python: pypy2
-    env: TOXENV=pypy
+  - python: 3.9
+    env: TOXENV=py39
+  - python: 3.10
+    env: TOXENV=py310
+#  - python: 3.11
+#    env: TOXENV=py311
 before_install:
   - "sudo apt-get update"
   - "sudo apt-get install python-gi python3-gi"
diff --git a/docs/conf.py b/docs/conf.py
index 161a7d337..ba82ef247 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 #
 # Urwid documentation build configuration file, created by
 # sphinx-quickstart on Wed Nov 30 20:10:17 2011.
@@ -11,7 +10,9 @@
 # All configuration values have a default; values that are commented out
 # serve to show the default.
 
-import sys, os
+from __future__ import annotations
+
+import os
 
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
@@ -40,24 +41,20 @@
 master_doc = 'index'
 
 # General information about the project.
-project = u'Urwid'
-copyright = u'2014, Ian Ward et al'
+project = 'Urwid'
+copyright = '2014, Ian Ward et al'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
-if sys.version_info > (3, 0):
-    FILE_PATH = os.path.dirname(__file__)
-else:
-    FILE_PATH = os.path.dirname(__file__).decode('utf-8')
+FILE_PATH = os.path.dirname(__file__)
 VERSION_MODULE = os.path.abspath(os.path.join(FILE_PATH,
     '../urwid/version.py'))
 VERSION_VARS = {}
-if sys.version_info > (3, 0):
-    def execfile(module, global_vars):
-        with open(module) as f:
-            code = compile(f.read(), "somefile.py", 'exec')
-            exec(code, global_vars)
+def execfile(module, global_vars):
+    with open(module) as f:
+        code = compile(f.read(), "somefile.py", 'exec')
+        exec(code, global_vars)
 execfile(VERSION_MODULE, VERSION_VARS)
 # The short X.Y version.
 version = '.'.join([str(x) for x in VERSION_VARS['VERSION'][:2]])
@@ -127,7 +124,7 @@ def execfile(module, global_vars):
 
 # The name for this set of Sphinx documents.  If None, it defaults to
 # "<project> v<release> documentation".
-html_title = "Urwid %s" % (release,)
+html_title = f"Urwid {release}"
 
 # A shorter title for the navigation bar.  Default is the same as html_title.
 #html_short_title = None
@@ -211,8 +208,8 @@ def execfile(module, global_vars):
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, documentclass [howto/manual]).
 latex_documents = [
-  ('index', 'Urwid.tex', u'Urwid Documentation',
-   u'Ian Ward', 'manual'),
+  ('index', 'Urwid.tex', 'Urwid Documentation',
+   'Ian Ward', 'manual'),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
@@ -241,8 +238,8 @@ def execfile(module, global_vars):
 # One entry per manual page. List of tuples
 # (source start file, name, description, authors, manual section).
 man_pages = [
-    ('index', 'urwid', u'Urwid Documentation',
-     [u'Ian Ward'], 1)
+    ('index', 'urwid', 'Urwid Documentation',
+     ['Ian Ward'], 1)
 ]
 
 # If true, show URL addresses after external links.
@@ -255,8 +252,8 @@ def execfile(module, global_vars):
 # (source start file, target name, title, author,
 #  dir menu entry, description, category)
 texinfo_documents = [
-  ('index', 'Urwid', u'Urwid Documentation',
-   u'Ian Ward', 'Urwid', 'One line description of project.',
+  ('index', 'Urwid', 'Urwid Documentation',
+   'Ian Ward', 'Urwid', 'One line description of project.',
    'Miscellaneous'),
 ]
 
@@ -273,10 +270,10 @@ def execfile(module, global_vars):
 # -- Options for Epub output ---------------------------------------------------
 
 # Bibliographic Dublin Core info.
-epub_title = u'Urwid'
-epub_author = u'Ian Ward'
-epub_publisher = u'Ian Ward'
-epub_copyright = u'2014, Ian Ward et al'
+epub_title = 'Urwid'
+epub_author = 'Ian Ward'
+epub_publisher = 'Ian Ward'
+epub_copyright = '2014, Ian Ward et al'
 
 # The language of the text. It defaults to the language option
 # or en if the language is not set.
diff --git a/docs/examples/browse.py b/docs/examples/browse.py
index 2bbcc3455..6d64cacad 100755
--- a/docs/examples/browse.py
+++ b/docs/examples/browse.py
@@ -1,6 +1,10 @@
 #!/usr/bin/env python
 
-import real_browse
+from __future__ import annotations
+
 import os
+
+import real_browse
+
 os.chdir('/usr/share/doc/python')
 real_browse.main()
diff --git a/docs/examples/edit.py b/docs/examples/edit.py
index 7eacc864d..18c306650 100755
--- a/docs/examples/edit.py
+++ b/docs/examples/edit.py
@@ -1,7 +1,10 @@
 #!/usr/bin/env python
 
-import sys
+from __future__ import annotations
+
 import os
+import sys
+
 import real_edit
 
 real_edit.EditDisplay(os.path.join(
diff --git a/docs/examples/subproc2.py b/docs/examples/subproc2.py
index 76c80903d..003ffad51 100644
--- a/docs/examples/subproc2.py
+++ b/docs/examples/subproc2.py
@@ -1,10 +1,12 @@
+from __future__ import annotations
 
 import time
+
 time.sleep(1)
 
-print """factor: 1
+print("""factor: 1
 factor: 1000003
 factor: 2000029
 factor: 3000073
 factor: 4000159
-factor: 5000101"""
+factor: 5000101""")
diff --git a/docs/manual/bright_combinations.py b/docs/manual/bright_combinations.py
index 4ff350c98..c566b2036 100755
--- a/docs/manual/bright_combinations.py
+++ b/docs/manual/bright_combinations.py
@@ -1,5 +1,7 @@
 #!/usr/bin/env python
 
+from __future__ import annotations
+
 import urwid
 
 RED_FGS = ('black', 'light gray', 'white', 'light cyan', 'light red',
@@ -27,8 +29,13 @@ def s(w):
     body.append(s(urwid.Divider()))
     body.append(s(
         urwid.GridFlow(
-            [urwid.AttrMap(urwid.Text("'{0}' on '{1}'".format(fg, bg)),
-                urwid.AttrSpec(fg, bg)) for fg in fgs],
+            [
+                urwid.AttrMap(
+                    urwid.Text(f"'{fg}' on '{bg}'"),
+                    urwid.AttrSpec(fg, bg)
+                )
+                for fg in fgs
+            ],
             35, 0, 0, 'left')))
     body.append(s(urwid.Divider()))
 
diff --git a/docs/manual/safe_combinations.py b/docs/manual/safe_combinations.py
index 5a02159e6..0da3337c9 100755
--- a/docs/manual/safe_combinations.py
+++ b/docs/manual/safe_combinations.py
@@ -1,5 +1,7 @@
 #!/usr/bin/env python
 
+from __future__ import annotations
+
 import urwid
 
 BLACK_FGS = ('light gray', 'dark cyan', 'dark red', 'dark green',
@@ -30,8 +32,13 @@ def s(w):
     body.append(s(urwid.Divider()))
     body.append(s(
         urwid.GridFlow(
-            [urwid.AttrMap(urwid.Text("'{0}' on '{1}'".format(fg, bg)),
-                urwid.AttrSpec(fg, bg)) for fg in fgs],
+            [
+                urwid.AttrMap(
+                    urwid.Text(f"'{fg}' on '{bg}'"),
+                    urwid.AttrSpec(fg, bg)
+                )
+                for fg in fgs
+            ],
             35, 0, 0, 'left')))
     body.append(s(urwid.Divider()))
 
diff --git a/docs/manual/wanat.py b/docs/manual/wanat.py
index 723eb9061..caf5c210c 100644
--- a/docs/manual/wanat.py
+++ b/docs/manual/wanat.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
 import urwid
 
+
 class Pudding(urwid.Widget):
     _sizing = frozenset(['flow'])
 
diff --git a/docs/manual/wanat_multi.py b/docs/manual/wanat_multi.py
index b6d69437b..9a1db870f 100644
--- a/docs/manual/wanat_multi.py
+++ b/docs/manual/wanat_multi.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
 import urwid
 
+
 class MultiPudding(urwid.Widget):
     _sizing = frozenset(['flow', 'box'])
 
diff --git a/docs/manual/wcur1.py b/docs/manual/wcur1.py
index 6bf97f736..0e177be8f 100644
--- a/docs/manual/wcur1.py
+++ b/docs/manual/wcur1.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
 import urwid
 
+
 class CursorPudding(urwid.Widget):
     _sizing = frozenset(['flow'])
     _selectable = True
diff --git a/docs/manual/wcur2.py b/docs/manual/wcur2.py
index 122541993..28ee07af5 100644
--- a/docs/manual/wcur2.py
+++ b/docs/manual/wcur2.py
@@ -1,7 +1,13 @@
-    def get_pref_col(self, (maxcol,)):
+from __future__ import annotations
+
+import urwid
+
+
+class Widget(urwid.ListBox):
+    def get_pref_col(self, size):
         return self.cursor_x
 
-    def move_cursor_to_coords(self, (maxcol,), col, row):
+    def move_cursor_to_coords(self, size, col, row):
         assert row == 0
         self.cursor_x = col
         return True
diff --git a/docs/manual/widgets.rst b/docs/manual/widgets.rst
index 9fabb97b8..d7da2596f 100644
--- a/docs/manual/widgets.rst
+++ b/docs/manual/widgets.rst
@@ -749,6 +749,7 @@ specified) then this method must move the cursor to that position and return
 
 .. literalinclude:: wcur2.py
    :linenos:
+   :lines: 7-
 
 
 Widget Metaclass
diff --git a/docs/manual/wmod.py b/docs/manual/wmod.py
index a066a495e..d25a07066 100644
--- a/docs/manual/wmod.py
+++ b/docs/manual/wmod.py
@@ -1,11 +1,14 @@
+from __future__ import annotations
+
 import urwid
 
+
 class QuestionnaireItem(urwid.WidgetWrap):
     def __init__(self):
         self.options = []
-        unsure = urwid.RadioButton(self.options, u"Unsure")
-        yes = urwid.RadioButton(self.options, u"Yes")
-        no = urwid.RadioButton(self.options, u"No")
+        unsure = urwid.RadioButton(self.options, "Unsure")
+        yes = urwid.RadioButton(self.options, "Yes")
+        no = urwid.RadioButton(self.options, "No")
         display_widget = urwid.GridFlow([unsure, yes, no], 15, 3, 1, 'left')
         urwid.WidgetWrap.__init__(self, display_widget)
 
diff --git a/docs/manual/wsel.py b/docs/manual/wsel.py
index 15683d8b7..2f7c0578c 100644
--- a/docs/manual/wsel.py
+++ b/docs/manual/wsel.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
 import urwid
 
+
 class SelectablePudding(urwid.Widget):
     _sizing = frozenset(['flow'])
     _selectable = True
diff --git a/docs/tutorial/adventure.py b/docs/tutorial/adventure.py
index 129ff50ab..0733cbe9c 100644
--- a/docs/tutorial/adventure.py
+++ b/docs/tutorial/adventure.py
@@ -1,17 +1,20 @@
+from __future__ import annotations
+
 import urwid
 
+
 class ActionButton(urwid.Button):
     def __init__(self, caption, callback):
-        super(ActionButton, self).__init__("")
+        super().__init__("")
         urwid.connect_signal(self, 'click', callback)
         self._w = urwid.AttrMap(urwid.SelectableIcon(caption, 1),
             None, focus_map='reversed')
 
 class Place(urwid.WidgetWrap):
     def __init__(self, name, choices):
-        super(Place, self).__init__(
-            ActionButton([u" > go to ", name], self.enter_place))
-        self.heading = urwid.Text([u"\nLocation: ", name, "\n"])
+        super().__init__(
+            ActionButton([" > go to ", name], self.enter_place))
+        self.heading = urwid.Text(["\nLocation: ", name, "\n"])
         self.choices = choices
         # create links back to ourself
         for child in choices:
@@ -22,41 +25,41 @@ def enter_place(self, button):
 
 class Thing(urwid.WidgetWrap):
     def __init__(self, name):
-        super(Thing, self).__init__(
-            ActionButton([u" * take ", name], self.take_thing))
+        super().__init__(
+            ActionButton([" * take ", name], self.take_thing))
         self.name = name
 
     def take_thing(self, button):
-        self._w = urwid.Text(u" - %s (taken)" % self.name)
+        self._w = urwid.Text(f" - {self.name} (taken)")
         game.take_thing(self)
 
 def exit_program(button):
     raise urwid.ExitMainLoop()
 
-map_top = Place(u'porch', [
-    Place(u'kitchen', [
-        Place(u'refrigerator', []),
-        Place(u'cupboard', [
-            Thing(u'jug'),
+map_top = Place('porch', [
+    Place('kitchen', [
+        Place('refrigerator', []),
+        Place('cupboard', [
+            Thing('jug'),
         ]),
     ]),
-    Place(u'garden', [
-        Place(u'tree', [
-            Thing(u'lemon'),
-            Thing(u'bird'),
+    Place('garden', [
+        Place('tree', [
+            Thing('lemon'),
+            Thing('bird'),
         ]),
     ]),
-    Place(u'street', [
-        Place(u'store', [
-            Thing(u'sugar'),
+    Place('street', [
+        Place('store', [
+            Thing('sugar'),
         ]),
-        Place(u'lake', [
-            Place(u'beach', []),
+        Place('lake', [
+            Place('beach', []),
         ]),
     ]),
 ])
 
-class AdventureGame(object):
+class AdventureGame:
     def __init__(self):
         self.log = urwid.SimpleFocusListWalker([])
         self.top = urwid.ListBox(self.log)
@@ -72,9 +75,9 @@ def update_place(self, place):
 
     def take_thing(self, thing):
         self.inventory.add(thing.name)
-        if self.inventory >= set([u'sugar', u'lemon', u'jug']):
-            response = urwid.Text(u'You can make lemonade!\n')
-            done = ActionButton(u' - Joy', exit_program)
+        if self.inventory >= {'sugar', 'lemon', 'jug'}:
+            response = urwid.Text('You can make lemonade!\n')
+            done = ActionButton(' - Joy', exit_program)
             self.log[:] = [response, done]
         else:
             self.update_place(self.place)
diff --git a/docs/tutorial/attr.py b/docs/tutorial/attr.py
index ce27171d7..9a6d37a59 100644
--- a/docs/tutorial/attr.py
+++ b/docs/tutorial/attr.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
 import urwid
 
+
 def exit_on_q(key):
     if key in ('q', 'Q'):
         raise urwid.ExitMainLoop()
@@ -9,7 +12,7 @@ def exit_on_q(key):
     ('streak', 'black', 'dark red'),
     ('bg', 'black', 'dark blue'),]
 
-txt = urwid.Text(('banner', u" Hello World "), align='center')
+txt = urwid.Text(('banner', " Hello World "), align='center')
 map1 = urwid.AttrMap(txt, 'streak')
 fill = urwid.Filler(map1)
 map2 = urwid.AttrMap(fill, 'bg')
diff --git a/docs/tutorial/cmenu.py b/docs/tutorial/cmenu.py
index 65c69c013..22c40721f 100644
--- a/docs/tutorial/cmenu.py
+++ b/docs/tutorial/cmenu.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
 import urwid
 
+
 def menu_button(caption, callback):
     button = urwid.Button(caption)
     urwid.connect_signal(button, 'click', callback)
@@ -9,7 +12,7 @@ def sub_menu(caption, choices):
     contents = menu(caption, choices)
     def open_menu(button):
         return top.open_box(contents)
-    return menu_button([caption, u'...'], open_menu)
+    return menu_button([caption, '...'], open_menu)
 
 def menu(title, choices):
     body = [urwid.Text(title), urwid.Divider()]
@@ -17,25 +20,25 @@ def menu(title, choices):
     return urwid.ListBox(urwid.SimpleFocusListWalker(body))
 
 def item_chosen(button):
-    response = urwid.Text([u'You chose ', button.label, u'\n'])
-    done = menu_button(u'Ok', exit_program)
+    response = urwid.Text(['You chose ', button.label, '\n'])
+    done = menu_button('Ok', exit_program)
     top.open_box(urwid.Filler(urwid.Pile([response, done])))
 
 def exit_program(button):
     raise urwid.ExitMainLoop()
 
-menu_top = menu(u'Main Menu', [
-    sub_menu(u'Applications', [
-        sub_menu(u'Accessories', [
-            menu_button(u'Text Editor', item_chosen),
-            menu_button(u'Terminal', item_chosen),
+menu_top = menu('Main Menu', [
+    sub_menu('Applications', [
+        sub_menu('Accessories', [
+            menu_button('Text Editor', item_chosen),
+            menu_button('Terminal', item_chosen),
         ]),
     ]),
-    sub_menu(u'System', [
-        sub_menu(u'Preferences', [
-            menu_button(u'Appearance', item_chosen),
+    sub_menu('System', [
+        sub_menu('Preferences', [
+            menu_button('Appearance', item_chosen),
         ]),
-        menu_button(u'Lock Screen', item_chosen),
+        menu_button('Lock Screen', item_chosen),
     ]),
 ])
 
@@ -43,7 +46,7 @@ class CascadingBoxes(urwid.WidgetPlaceholder):
     max_box_levels = 4
 
     def __init__(self, box):
-        super(CascadingBoxes, self).__init__(urwid.SolidFill(u'/'))
+        super().__init__(urwid.SolidFill('/'))
         self.box_level = 0
         self.open_box(box)
 
@@ -64,7 +67,7 @@ def keypress(self, size, key):
             self.original_widget = self.original_widget[0]
             self.box_level -= 1
         else:
-            return super(CascadingBoxes, self).keypress(size, key)
+            return super().keypress(size, key)
 
 top = CascadingBoxes(menu_top)
 urwid.MainLoop(top, palette=[('reversed', 'standout', '')]).run()
diff --git a/docs/tutorial/highcolors.py b/docs/tutorial/highcolors.py
index fd36395e1..5b6f4d8ac 100644
--- a/docs/tutorial/highcolors.py
+++ b/docs/tutorial/highcolors.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
 import urwid
 
+
 def exit_on_q(key):
     if key in ('q', 'Q'):
         raise urwid.ExitMainLoop()
@@ -20,7 +23,7 @@ def exit_on_q(key):
 div = urwid.Divider()
 outside = urwid.AttrMap(div, 'outside')
 inside = urwid.AttrMap(div, 'inside')
-txt = urwid.Text(('banner', u" Hello World "), align='center')
+txt = urwid.Text(('banner', " Hello World "), align='center')
 streak = urwid.AttrMap(txt, 'streak')
 pile = loop.widget.base_widget # .base_widget skips the decorations
 for item in [outside, inside, streak, inside, outside]:
diff --git a/docs/tutorial/hmenu.py b/docs/tutorial/hmenu.py
index c48f42b81..6c4ed1503 100644
--- a/docs/tutorial/hmenu.py
+++ b/docs/tutorial/hmenu.py
@@ -1,19 +1,22 @@
+from __future__ import annotations
+
 import urwid
 
+
 class MenuButton(urwid.Button):
     def __init__(self, caption, callback):
-        super(MenuButton, self).__init__("")
+        super().__init__("")
         urwid.connect_signal(self, 'click', callback)
         self._w = urwid.AttrMap(urwid.SelectableIcon(
-            [u'  \N{BULLET} ', caption], 2), None, 'selected')
+            ['  \N{BULLET} ', caption], 2), None, 'selected')
 
 class SubMenu(urwid.WidgetWrap):
     def __init__(self, caption, choices):
-        super(SubMenu, self).__init__(MenuButton(
-            [caption, u"\N{HORIZONTAL ELLIPSIS}"], self.open_menu))
-        line = urwid.Divider(u'\N{LOWER ONE QUARTER BLOCK}')
+        super().__init__(MenuButton(
+            [caption, "\N{HORIZONTAL ELLIPSIS}"], self.open_menu))
+        line = urwid.Divider('\N{LOWER ONE QUARTER BLOCK}')
         listbox = urwid.ListBox(urwid.SimpleFocusListWalker([
-            urwid.AttrMap(urwid.Text([u"\n  ", caption]), 'heading'),
+            urwid.AttrMap(urwid.Text(["\n  ", caption]), 'heading'),
             urwid.AttrMap(line, 'line'),
             urwid.Divider()] + choices + [urwid.Divider()]))
         self.menu = urwid.AttrMap(listbox, 'options')
@@ -23,31 +26,31 @@ def open_menu(self, button):
 
 class Choice(urwid.WidgetWrap):
     def __init__(self, caption):
-        super(Choice, self).__init__(
+        super().__init__(
             MenuButton(caption, self.item_chosen))
         self.caption = caption
 
     def item_chosen(self, button):
-        response = urwid.Text([u'  You chose ', self.caption, u'\n'])
-        done = MenuButton(u'Ok', exit_program)
+        response = urwid.Text(['  You chose ', self.caption, '\n'])
+        done = MenuButton('Ok', exit_program)
         response_box = urwid.Filler(urwid.Pile([response, done]))
         top.open_box(urwid.AttrMap(response_box, 'options'))
 
 def exit_program(key):
     raise urwid.ExitMainLoop()
 
-menu_top = SubMenu(u'Main Menu', [
-    SubMenu(u'Applications', [
-        SubMenu(u'Accessories', [
-            Choice(u'Text Editor'),
-            Choice(u'Terminal'),
+menu_top = SubMenu('Main Menu', [
+    SubMenu('Applications', [
+        SubMenu('Accessories', [
+            Choice('Text Editor'),
+            Choice('Terminal'),
         ]),
     ]),
-    SubMenu(u'System', [
-        SubMenu(u'Preferences', [
-            Choice(u'Appearance'),
+    SubMenu('System', [
+        SubMenu('Preferences', [
+            Choice('Appearance'),
         ]),
-        Choice(u'Lock Screen'),
+        Choice('Lock Screen'),
     ]),
 ])
 
@@ -67,7 +70,7 @@ def exit_program(key):
 
 class HorizontalBoxes(urwid.Columns):
     def __init__(self):
-        super(HorizontalBoxes, self).__init__([], dividechars=1)
+        super().__init__([], dividechars=1)
 
     def open_box(self, box):
         if self.contents:
diff --git a/docs/tutorial/input.py b/docs/tutorial/input.py
index 62a61a7ae..52541d149 100644
--- a/docs/tutorial/input.py
+++ b/docs/tutorial/input.py
@@ -1,11 +1,14 @@
+from __future__ import annotations
+
 import urwid
 
+
 def show_or_exit(key):
     if key in ('q', 'Q'):
         raise urwid.ExitMainLoop()
     txt.set_text(repr(key))
 
-txt = urwid.Text(u"Hello World")
+txt = urwid.Text("Hello World")
 fill = urwid.Filler(txt, 'top')
 loop = urwid.MainLoop(fill, unhandled_input=show_or_exit)
 loop.run()
diff --git a/docs/tutorial/minimal.py b/docs/tutorial/minimal.py
index 98acefa3b..57bdb5fa8 100644
--- a/docs/tutorial/minimal.py
+++ b/docs/tutorial/minimal.py
@@ -1,6 +1,8 @@
+from __future__ import annotations
+
 import urwid
 
-txt = urwid.Text(u"Hello World")
+txt = urwid.Text("Hello World")
 fill = urwid.Filler(txt, 'top')
 loop = urwid.MainLoop(fill)
 loop.run()
diff --git a/docs/tutorial/multiple.py b/docs/tutorial/multiple.py
index b2bc8a472..ecca9f07c 100644
--- a/docs/tutorial/multiple.py
+++ b/docs/tutorial/multiple.py
@@ -1,18 +1,21 @@
+from __future__ import annotations
+
 import urwid
 
+
 def question():
-    return urwid.Pile([urwid.Edit(('I say', u"What is your name?\n"))])
+    return urwid.Pile([urwid.Edit(('I say', "What is your name?\n"))])
 
 def answer(name):
-    return urwid.Text(('I say', u"Nice to meet you, " + name + "\n"))
+    return urwid.Text(('I say', f"Nice to meet you, {name}\n"))
 
 class ConversationListBox(urwid.ListBox):
     def __init__(self):
         body = urwid.SimpleFocusListWalker([question()])
-        super(ConversationListBox, self).__init__(body)
+        super().__init__(body)
 
     def keypress(self, size, key):
-        key = super(ConversationListBox, self).keypress(size, key)
+        key = super().keypress(size, key)
         if key != 'enter':
             return key
         name = self.focus[0].edit_text
diff --git a/docs/tutorial/qa.py b/docs/tutorial/qa.py
index b7d98170d..dc9590a4a 100644
--- a/docs/tutorial/qa.py
+++ b/docs/tutorial/qa.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
 import urwid
 
+
 def exit_on_q(key):
     if key in ('q', 'Q'):
         raise urwid.ExitMainLoop()
@@ -7,12 +10,11 @@ def exit_on_q(key):
 class QuestionBox(urwid.Filler):
     def keypress(self, size, key):
         if key != 'enter':
-            return super(QuestionBox, self).keypress(size, key)
+            return super().keypress(size, key)
         self.original_widget = urwid.Text(
-            u"Nice to meet you,\n%s.\n\nPress Q to exit." %
-            edit.edit_text)
+            f"Nice to meet you,\n{edit.edit_text}.\n\nPress Q to exit.")
 
-edit = urwid.Edit(u"What is your name?\n")
+edit = urwid.Edit("What is your name?\n")
 fill = QuestionBox(edit)
 loop = urwid.MainLoop(fill, unhandled_input=exit_on_q)
 loop.run()
diff --git a/docs/tutorial/sig.py b/docs/tutorial/sig.py
index c01c1b234..383553ebd 100644
--- a/docs/tutorial/sig.py
+++ b/docs/tutorial/sig.py
@@ -1,15 +1,17 @@
+from __future__ import annotations
+
 import urwid
 
 palette = [('I say', 'default,bold', 'default', 'bold'),]
-ask = urwid.Edit(('I say', u"What is your name?\n"))
-reply = urwid.Text(u"")
-button = urwid.Button(u'Exit')
+ask = urwid.Edit(('I say', "What is your name?\n"))
+reply = urwid.Text("")
+button = urwid.Button('Exit')
 div = urwid.Divider()
 pile = urwid.Pile([ask, div, reply, div, button])
 top = urwid.Filler(pile, valign='top')
 
 def on_ask_change(edit, new_edit_text):
-    reply.set_text(('I say', u"Nice to meet you, %s" % new_edit_text))
+    reply.set_text(('I say', f"Nice to meet you, {new_edit_text}"))
 
 def on_exit_clicked(button):
     raise urwid.ExitMainLoop()
diff --git a/docs/tutorial/smenu.py b/docs/tutorial/smenu.py
index dea050e06..115e1d38d 100644
--- a/docs/tutorial/smenu.py
+++ b/docs/tutorial/smenu.py
@@ -1,6 +1,8 @@
+from __future__ import annotations
+
 import urwid
 
-choices = u'Chapman Cleese Gilliam Idle Jones Palin'.split()
+choices = 'Chapman Cleese Gilliam Idle Jones Palin'.split()
 
 def menu(title, choices):
     body = [urwid.Text(title), urwid.Divider()]
@@ -11,8 +13,8 @@ def menu(title, choices):
     return urwid.ListBox(urwid.SimpleFocusListWalker(body))
 
 def item_chosen(button, choice):
-    response = urwid.Text([u'You chose ', choice, u'\n'])
-    done = urwid.Button(u'Ok')
+    response = urwid.Text(['You chose ', choice, '\n'])
+    done = urwid.Button('Ok')
     urwid.connect_signal(done, 'click', exit_program)
     main.original_widget = urwid.Filler(urwid.Pile([response,
         urwid.AttrMap(done, None, focus_map='reversed')]))
@@ -20,8 +22,8 @@ def item_chosen(button, choice):
 def exit_program(button):
     raise urwid.ExitMainLoop()
 
-main = urwid.Padding(menu(u'Pythons', choices), left=2, right=2)
-top = urwid.Overlay(main, urwid.SolidFill(u'\N{MEDIUM SHADE}'),
+main = urwid.Padding(menu('Pythons', choices), left=2, right=2)
+top = urwid.Overlay(main, urwid.SolidFill('\N{MEDIUM SHADE}'),
     align='center', width=('relative', 60),
     valign='middle', height=('relative', 60),
     min_width=20, min_height=9)
diff --git a/examples/asyncio_socket_server.py b/examples/asyncio_socket_server.py
index 2833faff9..e629d74b0 100644
--- a/examples/asyncio_socket_server.py
+++ b/examples/asyncio_socket_server.py
@@ -4,22 +4,18 @@
 This code works on older Python 3.x if you install `asyncio` from PyPI, and
 even Python 2 if you install `trollius`!
 """
-from __future__ import print_function
 
-try:
-    import asyncio
-except ImportError:
-    import trollius as asyncio
+from __future__ import annotations
 
-from datetime import datetime
+import asyncio
+import logging
 import sys
 import weakref
+from datetime import datetime
 
 import urwid
 from urwid.raw_display import Screen
-from urwid.display_common import BaseScreen
 
-import logging
 logging.basicConfig()
 
 loop = asyncio.get_event_loop()
diff --git a/examples/bigtext.py b/examples/bigtext.py
index 6a088ea68..2a19fe3b6 100755
--- a/examples/bigtext.py
+++ b/examples/bigtext.py
@@ -17,12 +17,14 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
 """
 Urwid example demonstrating use of the BigText widget.
 """
 
+from __future__ import annotations
+
 import urwid
 import urwid.raw_display
 
@@ -58,7 +60,7 @@ def create_radio_button(self, g, name, font, fn):
         return w
 
     def create_disabled_radio_button(self, name):
-        w = urwid.Text("    " + name + " (UTF-8 mode required)")
+        w = urwid.Text(f"    {name} (UTF-8 mode required)")
         w = urwid.AttrWrap(w, 'button disabled')
         return w
 
@@ -110,7 +112,7 @@ def setup_view(self):
         chosen_font_rb.set_state(True) # causes set_font_event call
 
         # Create Edit widget
-        edit = self.create_edit("", "Urwid "+urwid.__version__,
+        edit = self.create_edit("", f"Urwid {urwid.__version__}",
             self.edit_change_event)
 
         # ListBox
diff --git a/examples/browse.py b/examples/browse.py
index 39ac27a94..37315b105 100755
--- a/examples/browse.py
+++ b/examples/browse.py
@@ -19,7 +19,7 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
 """
 Urwid example lazy directory browser / tree view
@@ -31,11 +31,12 @@
 - outputs a quoted list of files and directories "selected" on exit
 """
 
-from __future__ import print_function
+
+from __future__ import annotations
 
 import itertools
-import re
 import os
+import re
 
 import urwid
 
@@ -48,7 +49,7 @@ class FlagFileWidget(urwid.TreeWidget):
         'dirmark')
 
     def __init__(self, node):
-        self.__super.__init__(node)
+        super().__init__(node)
         # insert an extra AttrWrap for our own use
         self._w = urwid.AttrWrap(self._w, None)
         self.flagged = False
@@ -59,7 +60,7 @@ def selectable(self):
 
     def keypress(self, size, key):
         """allow subclasses to intercept keystrokes"""
-        key = self.__super.keypress(size, key)
+        key = super().keypress(size, key)
         if key:
             key = self.unhandled_keys(size, key)
         return key
@@ -89,7 +90,7 @@ def update_w(self):
 class FileTreeWidget(FlagFileWidget):
     """Widget for individual files."""
     def __init__(self, node):
-        self.__super.__init__(node)
+        super().__init__(node)
         path = node.get_value()
         add_widget(path, self)
 
@@ -114,7 +115,7 @@ def get_display_text(self):
 class DirectoryWidget(FlagFileWidget):
     """Widget for a directory."""
     def __init__(self, node):
-        self.__super.__init__(node)
+        super().__init__(node)
         path = node.get_value()
         add_widget(path, self)
         self.expanded = starts_expanded(path)
@@ -350,7 +351,7 @@ def escape_filename_sh(name):
     name.replace('"','\\"')
     name.replace('`','\\`')
     name.replace('$','\\$')
-    return '"'+name+'"'
+    return f'"{name}"'
 
 
 def escape_filename_sh_ansic(name):
@@ -360,14 +361,14 @@ def escape_filename_sh_ansic(name):
     # gather the escaped characters into a list
     for ch in name:
         if ord(ch) < 32:
-            out.append("\\x%02x"% ord(ch))
+            out.append(f"\\x{ord(ch):02x}")
         elif ch == '\\':
             out.append('\\\\')
         else:
             out.append(ch)
 
     # slap them back together in an ansi-c quote  $'...'
-    return "$'" + "".join(out) + "'"
+    return f"$'{''.join(out)}'"
 
 SPLIT_RE = re.compile(r'[a-zA-Z]+|\d+')
 def alphabetize(s):
diff --git a/examples/calc.py b/examples/calc.py
index 7b47fe9a3..bf31ba6c9 100755
--- a/examples/calc.py
+++ b/examples/calc.py
@@ -17,7 +17,7 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
 """
 Urwid advanced example column calculator application
@@ -31,7 +31,8 @@
 - outputs commands that may be used to recreate expression on exit
 """
 
-from __future__ import print_function
+
+from __future__ import annotations
 
 import urwid
 import urwid.raw_display
@@ -137,7 +138,7 @@ def setup_edit(self):
 
         self.edit = urwid.IntEdit()
         if not self.is_top:
-            self.edit.set_caption( self.op + " " )
+            self.edit.set_caption( f"{self.op} " )
         self.edit.set_layout( None, None, CALC_LAYOUT )
 
     def get_value(self):
@@ -146,7 +147,7 @@ def get_value(self):
         if self.child is not None:
             return self.child.get_result()
         else:
-            return int("0"+self.edit.edit_text)
+            return int(f"0{self.edit.edit_text}")
 
     def get_result(self):
         """Return the numeric result of this cell's operation."""
@@ -203,9 +204,9 @@ def set_letter(self, letter):
         """Set the letter of the child column for display."""
 
         self.letter = letter
-        caption = "("+letter+")"
+        caption = f"({letter})"
         if self.op is not None:
-            caption = self.op+" "+caption
+            caption = f"{self.op} {caption}"
         self.set_caption(caption)
 
     def keypress(self, size, key):
@@ -478,7 +479,7 @@ def get_expression(self):
             if c.op is not None: # only applies to first cell
                 l.append(c.op)
             if c.child is not None:
-                l.append("("+c.child.get_expression()+")")
+                l.append(f"({c.child.get_expression()})")
             else:
                 l.append("%d"%c.get_value())
 
diff --git a/examples/dialog.py b/examples/dialog.py
index b6359338c..ec8c8e0a1 100755
--- a/examples/dialog.py
+++ b/examples/dialog.py
@@ -17,13 +17,15 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
 """
 Urwid example similar to dialog(1) program
 
 """
 
+from __future__ import annotations
+
 import sys
 
 import urwid
@@ -215,7 +217,7 @@ def on_exit(self, exitcode):
         for i in self.items:
             if i.get_state():
                 l.append(i.get_label())
-        return exitcode, "".join(['"'+tag+'" ' for tag in l])
+        return exitcode, "".join([f'"{tag}" ' for tag in l])
 
 
 
@@ -307,8 +309,8 @@ def show_usage():
     """
     Display a helpful usage message.
     """
-    modelist = [(mode, help) for (mode, (fn, help)) in MODES.items()]
-    modelist.sort()
+    modelist = sorted((mode, help) for (mode, (fn, help)) in MODES.items())
+
     sys.stdout.write(
         __doc__ +
         "\n".join(["%-15s %s"%(mode,help) for (mode,help) in modelist])
@@ -334,7 +336,7 @@ def main():
 
     # Exit
     if exitstring:
-        sys.stderr.write(exitstring+"\n")
+        sys.stderr.write(f"{exitstring}\n")
 
     sys.exit(exitcode)
 
diff --git a/examples/edit.py b/examples/edit.py
index e2cc2faab..e3da36bbb 100755
--- a/examples/edit.py
+++ b/examples/edit.py
@@ -17,7 +17,7 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
 """
 Urwid example lazy text editor suitable for tabbed and flowing text
@@ -30,6 +30,8 @@
 
 """
 
+from __future__ import annotations
+
 import sys
 
 import urwid
@@ -230,7 +232,7 @@ def re_tab(s):
     for i in range(8, len(s), 8):
         if s[i-2:i] == "  ":
             # collapse two or more spaces into a tab
-            l.append(s[p:i].rstrip() + "\t")
+            l.append(f"{s[p:i].rstrip()}\t")
             p = i
 
     if p == 0:
diff --git a/examples/fib.py b/examples/fib.py
index 875c8fc24..47fc8c9d0 100755
--- a/examples/fib.py
+++ b/examples/fib.py
@@ -17,7 +17,7 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
 """
 Urwid example fibonacci sequence viewer / unbounded data demo
@@ -27,8 +27,11 @@
 - custom wrap mode "numeric" for wrapping numbers to right and bottom
 """
 
+from __future__ import annotations
+
 import urwid
 
+
 class FibonacciWalker(urwid.ListWalker):
     """ListWalker-compatible class for browsing fibonacci set.
 
diff --git a/examples/graph.py b/examples/graph.py
index 4a5b5dd7f..ad2bf158d 100755
--- a/examples/graph.py
+++ b/examples/graph.py
@@ -17,7 +17,7 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
 """
 Urwid example demonstrating use of the BarGraph widget and creating a
@@ -25,11 +25,13 @@
 animation.
 """
 
-import urwid
+from __future__ import annotations
 
 import math
 import time
 
+import urwid
+
 UPDATE_INTERVAL = 0.2
 
 def sin100( x ):
@@ -203,8 +205,8 @@ def on_unicode_checkbox(self, w, state):
 
     def main_shadow(self, w):
         """Wrap a shadow and background around widget w."""
-        bg = urwid.AttrWrap(urwid.SolidFill(u"\u2592"), 'screen edge')
-        shadow = urwid.AttrWrap(urwid.SolidFill(u" "), 'main shadow')
+        bg = urwid.AttrWrap(urwid.SolidFill("\u2592"), 'screen edge')
+        shadow = urwid.AttrWrap(urwid.SolidFill(" "), 'main shadow')
 
         bg = urwid.Overlay( shadow, bg,
             ('fixed left', 3), ('fixed right', 1),
@@ -288,7 +290,7 @@ def graph_controls(self):
     def main_window(self):
         self.graph = self.bar_graph()
         self.graph_wrap = urwid.WidgetWrap( self.graph )
-        vline = urwid.AttrWrap( urwid.SolidFill(u'\u2502'), 'line')
+        vline = urwid.AttrWrap( urwid.SolidFill('\u2502'), 'line')
         c = self.graph_controls()
         w = urwid.Columns([('weight',2,self.graph_wrap),
             ('fixed',1,vline), c],
diff --git a/examples/lcd_cf635.py b/examples/lcd_cf635.py
index c30a3d26d..cd536bf22 100755
--- a/examples/lcd_cf635.py
+++ b/examples/lcd_cf635.py
@@ -18,7 +18,10 @@
 respectively.
 """
 
+from __future__ import annotations
+
 import sys
+
 import urwid.lcd_display
 
 CGRAM = """
@@ -136,7 +139,7 @@ def __init__(self, range, value, callback):
             self.bar,
             ('fixed', 1, urwid.SelectableIcon('\x04')),
             ])
-        self.__super.__init__(cols)
+        super().__init__(cols)
         self.callback = callback
 
     def keypress(self, size, key):
@@ -147,7 +150,7 @@ def keypress(self, size, key):
                 self._w.get_focus_column() != 0)
             self.callback(self.bar.value)
         else:
-            return self.__super.keypress(size, key)
+            return super().keypress(size, key)
 
 
 
@@ -156,7 +159,7 @@ class MenuOption(urwid.Button):
     A menu option, indicated with a single arrow character
     """
     def __init__(self, label, submenu):
-        self.__super.__init__("")
+        super().__init__("")
         # use a Text widget for label, we want the cursor
         # on the arrow not the label
         self._label = urwid.Text("")
@@ -172,19 +175,19 @@ def __init__(self, label, submenu):
     def keypress(self, size, key):
         if key == 'right':
             key = 'enter'
-        return self.__super.keypress(size, key)
+        return super().keypress(size, key)
 
 
 class Menu(urwid.ListBox):
     def __init__(self, widgets):
         self.menu_parent = None
-        self.__super.__init__(urwid.SimpleListWalker(widgets))
+        super().__init__(urwid.SimpleListWalker(widgets))
 
     def keypress(self, size, key):
         """
         Go back to the previous menu on cancel button (mapped to esc)
         """
-        key = self.__super.keypress(size, key)
+        key = super().keypress(size, key)
         if key in ('left', 'esc') and self.menu_parent:
             show_menu(self.menu_parent)
         else:
diff --git a/examples/pop_up.py b/examples/pop_up.py
index f88c71790..a87c7e366 100755
--- a/examples/pop_up.py
+++ b/examples/pop_up.py
@@ -1,7 +1,10 @@
 #!/usr/bin/env python
 
+from __future__ import annotations
+
 import urwid
 
+
 class PopUpDialog(urwid.WidgetWrap):
     """A dialog that appears with nothing but a close button """
     signals = ['close']
@@ -13,12 +16,12 @@ def __init__(self):
             "^^  I'm attached to the widget that opened me. "
             "Try resizing the window!\n"), close_button])
         fill = urwid.Filler(pile)
-        self.__super.__init__(urwid.AttrWrap(fill, 'popbg'))
+        super().__init__(urwid.AttrWrap(fill, 'popbg'))
 
 
 class ThingWithAPopUp(urwid.PopUpLauncher):
     def __init__(self):
-        self.__super.__init__(urwid.Button("click-me"))
+        super().__init__(urwid.Button("click-me"))
         urwid.connect_signal(self.original_widget, 'click',
             lambda button: self.open_pop_up())
 
diff --git a/examples/subproc.py b/examples/subproc.py
index f04e7b0e7..b078219da 100755
--- a/examples/subproc.py
+++ b/examples/subproc.py
@@ -1,10 +1,13 @@
 #!/usr/bin/env python
 
-import subprocess
-import urwid
+from __future__ import annotations
+
 import os
+import subprocess
 import sys
 
+import urwid
+
 factor_me = 362923067964327863989661926737477737673859044111968554257667
 run_me = os.path.join(os.path.dirname(sys.argv[0]), 'subproc2.py')
 
diff --git a/examples/subproc2.py b/examples/subproc2.py
index a60aa72c0..39c08196c 100644
--- a/examples/subproc2.py
+++ b/examples/subproc2.py
@@ -1,14 +1,10 @@
 #!/usr/bin/env python
 # this is part of the subproc.py example
 
-from __future__ import print_function
 
-import sys
+from __future__ import annotations
 
-try:
-    range = xrange
-except NameError:
-    pass
+import sys
 
 num = int(sys.argv[1])
 for c in range(1,10000000):
diff --git a/examples/terminal.py b/examples/terminal.py
index c50656a0f..1fd952f62 100755
--- a/examples/terminal.py
+++ b/examples/terminal.py
@@ -17,10 +17,13 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
+
+from __future__ import annotations
 
 import urwid
 
+
 def main():
     urwid.set_encoding('utf8')
     term = urwid.Terminal(None, encoding='utf-8')
diff --git a/examples/tour.py b/examples/tour.py
index 7e6649fa5..7bc62c6d7 100755
--- a/examples/tour.py
+++ b/examples/tour.py
@@ -1,5 +1,4 @@
 #!/usr/bin/env python
-# coding: utf-8
 #
 # Urwid tour.  It slices, it dices..
 #    Copyright (C) 2004-2011  Ian Ward
@@ -18,125 +17,128 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
 """
 Urwid tour.  Shows many of the standard widget types and features.
 """
 
+from __future__ import annotations
+
 import urwid
 import urwid.raw_display
 import urwid.web_display
 
+
 def main():
-    text_header = (u"Welcome to the urwid tour!  "
-        u"UP / DOWN / PAGE UP / PAGE DOWN scroll.  F8 exits.")
-    text_intro = [('important', u"Text"),
-        u" widgets are the most common in "
-        u"any urwid program.  This Text widget was created "
-        u"without setting the wrap or align mode, so it "
-        u"defaults to left alignment with wrapping on space "
-        u"characters.  ",
-        ('important', u"Change the window width"),
-        u" to see how the widgets on this page react.  "
-        u"This Text widget is wrapped with a ",
-        ('important', u"Padding"),
-        u" widget to keep it indented on the left and right."]
-    text_right = (u"This Text widget is right aligned.  Wrapped "
-        u"words stay to the right as well. ")
-    text_center = u"This one is center aligned."
-    text_clip = (u"Text widgets may be clipped instead of wrapped.\n"
-        u"Extra text is discarded instead of wrapped to the next line. "
-        u"65-> 70-> 75-> 80-> 85-> 90-> 95-> 100>\n"
-        u"Newlines embedded in the string are still respected.")
-    text_right_clip = (u"This is a right aligned and clipped Text widget.\n"
-        u"<100 <-95 <-90 <-85 <-80 <-75 <-70 <-65             "
-        u"Text will be cut off at the left of this widget.")
-    text_center_clip = (u"Center aligned and clipped widgets will have "
-        u"text cut off both sides.")
-    text_ellipsis = (u"Text can be clipped using the ellipsis character (…)\n"
-        u"Extra text is discarded and a … mark is shown."
-         u"50-> 55-> 60-> 65-> 70-> 75-> 80-> 85-> 90-> 95-> 100>\n"
+    text_header = ("Welcome to the urwid tour!  "
+        "UP / DOWN / PAGE UP / PAGE DOWN scroll.  F8 exits.")
+    text_intro = [('important', "Text"),
+        " widgets are the most common in "
+        "any urwid program.  This Text widget was created "
+        "without setting the wrap or align mode, so it "
+        "defaults to left alignment with wrapping on space "
+        "characters.  ",
+        ('important', "Change the window width"),
+        " to see how the widgets on this page react.  "
+        "This Text widget is wrapped with a ",
+        ('important', "Padding"),
+        " widget to keep it indented on the left and right."]
+    text_right = ("This Text widget is right aligned.  Wrapped "
+        "words stay to the right as well. ")
+    text_center = "This one is center aligned."
+    text_clip = ("Text widgets may be clipped instead of wrapped.\n"
+        "Extra text is discarded instead of wrapped to the next line. "
+        "65-> 70-> 75-> 80-> 85-> 90-> 95-> 100>\n"
+        "Newlines embedded in the string are still respected.")
+    text_right_clip = ("This is a right aligned and clipped Text widget.\n"
+        "<100 <-95 <-90 <-85 <-80 <-75 <-70 <-65             "
+        "Text will be cut off at the left of this widget.")
+    text_center_clip = ("Center aligned and clipped widgets will have "
+        "text cut off both sides.")
+    text_ellipsis = ("Text can be clipped using the ellipsis character (…)\n"
+        "Extra text is discarded and a … mark is shown."
+         "50-> 55-> 60-> 65-> 70-> 75-> 80-> 85-> 90-> 95-> 100>\n"
     )
-    text_any = (u"The 'any' wrap mode will wrap on any character.  This "
-        u"mode will not collapse space characters at the end of the "
-        u"line but it still honors embedded newline characters.\n"
-        u"Like this one.")
-    text_padding = (u"Padding widgets have many options.  This "
-        u"is a standard Text widget wrapped with a Padding widget "
-        u"with the alignment set to relative 20% and with its width "
-        u"fixed at 40.")
-    text_divider =  [u"The ", ('important', u"Divider"),
-        u" widget repeats the same character across the whole line.  "
-        u"It can also add blank lines above and below."]
-    text_edit = [u"The ", ('important', u"Edit"),
-        u" widget is a simple text editing widget.  It supports cursor "
-        u"movement and tries to maintain the current column when focus "
-        u"moves to another edit widget.  It wraps and aligns the same "
-        u"way as Text widgets." ]
-    text_edit_cap1 = ('editcp', u"This is a caption.  Edit here: ")
-    text_edit_text1 = u"editable stuff"
-    text_edit_cap2 = ('editcp', u"This one supports newlines: ")
-    text_edit_text2 = (u"line one starts them all\n"
-        u"== line 2 == with some more text to edit.. words.. whee..\n"
-        u"LINE III, the line to end lines one and two, unless you "
-        u"change something.")
-    text_edit_cap3 = ('editcp', u"This one is clipped, try "
-        u"editing past the edge: ")
-    text_edit_text3 = u"add some text here -> -> -> ...."
-    text_edit_alignments = u"Different Alignments:"
-    text_edit_left = u"left aligned (default)"
-    text_edit_center = u"center aligned"
-    text_edit_right = u"right aligned"
-    text_intedit = ('editcp', [('important', u"IntEdit"),
-        u" allows only numbers: "])
-    text_edit_padding = ('editcp', u"Edit widget within a Padding widget ")
-    text_columns1 = [('important', u"Columns"),
-        u" are used to share horizontal screen space.  "
-        u"This one splits the space into two parts with "
-        u"three characters between each column.  The "
-        u"contents of each column is a single widget."]
-    text_columns2 = [u"When you need to put more than one "
-        u"widget into a column you can use a ",('important',
-        u"Pile"), u" to combine two or more widgets."]
-    text_col_columns = u"Columns may be placed inside other columns."
-    text_col_21 = u"Col 2.1"
-    text_col_22 = u"Col 2.2"
-    text_col_23 = u"Col 2.3"
-    text_column_widths = (u"Columns may also have uneven relative "
-        u"weights or fixed widths.  Use a minimum width so that "
-        u"columns don't become too small.")
-    text_weight = u"Weight %d"
-    text_fixed_9 = u"<Fixed 9>" # should be 9 columns wide
-    text_fixed_14 = u"<--Fixed 14-->" # should be 14 columns wide
-    text_edit_col_cap1 = ('editcp', u"Edit widget within Columns")
-    text_edit_col_text1 = u"here's\nsome\ninfo"
-    text_edit_col_cap2 = ('editcp', u"and within Pile ")
-    text_edit_col_text2 = u"more"
-    text_edit_col_cap3 = ('editcp', u"another ")
-    text_edit_col_text3 = u"still more"
-    text_gridflow = [u"A ",('important', u"GridFlow"), u" widget "
-        u"may be used to display a list of flow widgets with equal "
-        u"widths.  Widgets that don't fit on the first line will "
-        u"flow to the next.  This is useful for small widgets that "
-        u"you want to keep together such as ", ('important', u"Button"),
-        u", ",('important', u"CheckBox"), u" and ",
-        ('important', u"RadioButton"), u" widgets." ]
-    text_button_list = [u"Yes", u"No", u"Perhaps", u"Certainly", u"Partially",
-        u"Tuesdays Only", u"Help"]
-    text_cb_list = [u"Wax", u"Wash", u"Buff", u"Clear Coat", u"Dry",
-        u"Racing Stripe"]
-    text_rb_list = [u"Morning", u"Afternoon", u"Evening", u"Weekend"]
-    text_listbox = [u"All these widgets have been displayed "
-        u"with the help of a ", ('important', u"ListBox"), u" widget.  "
-        u"ListBox widgets handle scrolling and changing focus.  A ",
-        ('important', u"Frame"), u" widget is used to keep the "
-        u"instructions at the top of the screen."]
+    text_any = ("The 'any' wrap mode will wrap on any character.  This "
+        "mode will not collapse space characters at the end of the "
+        "line but it still honors embedded newline characters.\n"
+        "Like this one.")
+    text_padding = ("Padding widgets have many options.  This "
+        "is a standard Text widget wrapped with a Padding widget "
+        "with the alignment set to relative 20% and with its width "
+        "fixed at 40.")
+    text_divider =  ["The ", ('important', "Divider"),
+        " widget repeats the same character across the whole line.  "
+        "It can also add blank lines above and below."]
+    text_edit = ["The ", ('important', "Edit"),
+        " widget is a simple text editing widget.  It supports cursor "
+        "movement and tries to maintain the current column when focus "
+        "moves to another edit widget.  It wraps and aligns the same "
+        "way as Text widgets." ]
+    text_edit_cap1 = ('editcp', "This is a caption.  Edit here: ")
+    text_edit_text1 = "editable stuff"
+    text_edit_cap2 = ('editcp', "This one supports newlines: ")
+    text_edit_text2 = ("line one starts them all\n"
+        "== line 2 == with some more text to edit.. words.. whee..\n"
+        "LINE III, the line to end lines one and two, unless you "
+        "change something.")
+    text_edit_cap3 = ('editcp', "This one is clipped, try "
+        "editing past the edge: ")
+    text_edit_text3 = "add some text here -> -> -> ...."
+    text_edit_alignments = "Different Alignments:"
+    text_edit_left = "left aligned (default)"
+    text_edit_center = "center aligned"
+    text_edit_right = "right aligned"
+    text_intedit = ('editcp', [('important', "IntEdit"),
+        " allows only numbers: "])
+    text_edit_padding = ('editcp', "Edit widget within a Padding widget ")
+    text_columns1 = [('important', "Columns"),
+        " are used to share horizontal screen space.  "
+        "This one splits the space into two parts with "
+        "three characters between each column.  The "
+        "contents of each column is a single widget."]
+    text_columns2 = ["When you need to put more than one "
+        "widget into a column you can use a ",('important',
+        "Pile"), " to combine two or more widgets."]
+    text_col_columns = "Columns may be placed inside other columns."
+    text_col_21 = "Col 2.1"
+    text_col_22 = "Col 2.2"
+    text_col_23 = "Col 2.3"
+    text_column_widths = ("Columns may also have uneven relative "
+        "weights or fixed widths.  Use a minimum width so that "
+        "columns don't become too small.")
+    text_weight = "Weight %d"
+    text_fixed_9 = "<Fixed 9>" # should be 9 columns wide
+    text_fixed_14 = "<--Fixed 14-->" # should be 14 columns wide
+    text_edit_col_cap1 = ('editcp', "Edit widget within Columns")
+    text_edit_col_text1 = "here's\nsome\ninfo"
+    text_edit_col_cap2 = ('editcp', "and within Pile ")
+    text_edit_col_text2 = "more"
+    text_edit_col_cap3 = ('editcp', "another ")
+    text_edit_col_text3 = "still more"
+    text_gridflow = ["A ",('important', "GridFlow"), " widget "
+        "may be used to display a list of flow widgets with equal "
+        "widths.  Widgets that don't fit on the first line will "
+        "flow to the next.  This is useful for small widgets that "
+        "you want to keep together such as ", ('important', "Button"),
+        ", ",('important', "CheckBox"), " and ",
+        ('important', "RadioButton"), " widgets." ]
+    text_button_list = ["Yes", "No", "Perhaps", "Certainly", "Partially",
+        "Tuesdays Only", "Help"]
+    text_cb_list = ["Wax", "Wash", "Buff", "Clear Coat", "Dry",
+        "Racing Stripe"]
+    text_rb_list = ["Morning", "Afternoon", "Evening", "Weekend"]
+    text_listbox = ["All these widgets have been displayed "
+        "with the help of a ", ('important', "ListBox"), " widget.  "
+        "ListBox widgets handle scrolling and changing focus.  A ",
+        ('important', "Frame"), " widget is used to keep the "
+        "instructions at the top of the screen."]
 
 
     def button_press(button):
         frame.footer = urwid.AttrWrap(urwid.Text(
-            [u"Pressed: ", button.get_label()]), 'header')
+            ["Pressed: ", button.get_label()]), 'header')
 
     radio_button_group = []
 
diff --git a/examples/treesample.py b/examples/treesample.py
index 7b4f42ff9..e3e148a5d 100755
--- a/examples/treesample.py
+++ b/examples/treesample.py
@@ -20,7 +20,7 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
 """
 Urwid example lazy directory browser / tree view
@@ -31,8 +31,9 @@
 - custom list walker for displaying widgets in a tree fashion
 """
 
+from __future__ import annotations
+
 import urwid
-import os
 
 
 class ExampleTreeWidget(urwid.TreeWidget):
@@ -120,7 +121,7 @@ def get_example_tree():
     """ generate a quick 100 leaf tree for demo purposes """
     retval = {"name":"parent","children":[]}
     for i in range(10):
-        retval['children'].append({"name":"child " + str(i)})
+        retval['children'].append({"name":f"child {str(i)}"})
         retval['children'][i]['children']=[]
         for j in range(10):
             retval['children'][i]['children'].append({"name":"grandchild " +
diff --git a/examples/twisted_serve_ssh.py b/examples/twisted_serve_ssh.py
index b2e020634..4b8235b60 100644
--- a/examples/twisted_serve_ssh.py
+++ b/examples/twisted_serve_ssh.py
@@ -30,24 +30,26 @@
 Licence:   LGPL <http://opensource.org/licenses/lgpl-2.1.php>
 """
 
-from __future__ import print_function
 
-import os
+from __future__ import annotations
 
-import urwid
-from urwid.raw_display import Screen
-
-from zope.interface import Interface, Attribute, implements
-from twisted.application.service import Application
 from twisted.application.internet import TCPServer
-from twisted.cred.portal import Portal
+from twisted.application.service import Application
+from twisted.conch.insults.insults import ServerProtocol, TerminalProtocol
 from twisted.conch.interfaces import IConchUser, ISession
-from twisted.conch.insults.insults import TerminalProtocol, ServerProtocol
-from twisted.conch.manhole_ssh import (ConchFactory, TerminalRealm,
-    TerminalUser, TerminalSession, TerminalSessionTransport)
-
-from twisted.python.components import Componentized, Adapter
+from twisted.conch.manhole_ssh import (
+    ConchFactory,
+    TerminalRealm,
+    TerminalSession,
+    TerminalSessionTransport,
+    TerminalUser,
+)
+from twisted.cred.portal import Portal
+from twisted.python.components import Adapter, Componentized
+from zope.interface import Attribute, Interface, implements
 
+import urwid
+from urwid.raw_display import Screen
 
 
 class IUrwidUi(Interface):
@@ -84,7 +86,7 @@ def draw():
 
 
 
-class UrwidUi(object):
+class UrwidUi:
 
     def __init__(self, urwid_mind):
         self.mind = urwid_mind
@@ -111,7 +113,7 @@ def create_urwid_mainloop(self):
 
 
 
-class UnhandledKeyHandler(object):
+class UnhandledKeyHandler:
 
     def __init__(self, mind):
         self.mind = mind
@@ -120,7 +122,7 @@ def push(self, key):
         if isinstance(key, tuple):
             pass
         else:
-            f = getattr(self, 'key_%s' % key.replace(' ', '_'), None)
+            f = getattr(self, f"key_{key.replace(' ', '_')}", None)
             if f is None:
                 return
             else:
@@ -214,10 +216,10 @@ def draw_screen(self, maxres, r ):
             self.terminal.cursorPosition(0, i)
             for (attr, cs, text) in row:
                 if attr != lasta:
-                    text = '%s%s' % (self._attr_to_escape(attr), text)
+                    text = f'{self._attr_to_escape(attr)}{text}'
                 lasta = attr
                 #if cs or attr:
-                #    print cs, attr
+                #    print(cs, attr)
                 self.write(text)
         cursor = r.get_cursor()
         if cursor is not None:
@@ -325,7 +327,7 @@ def _attrspec_to_escape(self, a):
                 bg = "%d" % (a.background_number + 40)
         else:
             bg = "49"
-        return urwid.escape.ESC + "[0;%s;%s%sm" % (fg, st, bg)
+        return f"{urwid.escape.ESC}[0;{fg};{st}{bg}m"
 
 
 class UrwidTerminalProtocol(TerminalProtocol):
diff --git a/setup.py b/setup.py
index 68f7adbc9..aad73f2e3 100644
--- a/setup.py
+++ b/setup.py
@@ -17,41 +17,36 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-try:
-    PYTHON3 = not str is bytes
-except NameError:
-    PYTHON3 = False
-
-try:
-    from setuptools import setup, Extension # distribute required for Python 3
-    have_setuptools = True
-except ImportError:
-    if PYTHON3:
-        raise
-    from distutils.core import setup, Extension
-    have_setuptools = False
+from __future__ import annotations
 
 import os
 
-exec(open(os.path.join("urwid","version.py")).read())
-release = __version__
+from setuptools import Extension, setup
+
+with open(os.path.join("urwid", "version.py")) as f:
+    globals_ = {}
+    locals_ = {}
+    # Execute safe way: do not expose anything
+    exec(f.read(), globals_, locals_)
+    release = locals_['__version__']
+
+
+with open("README.rst") as f:
+    long_description = f.read().split('.. content-start\n', 1)[1]
+
 
 setup_d = {
-    'name':"urwid",
-    'version':release,
-    'author':"Ian Ward",
-    'author_email':"ian@excess.org",
-    'ext_modules':[Extension('urwid.str_util', sources=['source/str_util.c'])],
-    'packages':['urwid', 'urwid.tests'],
-    'url':"http://urwid.org/",
-    'license':"LGPL",
-    'keywords':"curses ui widget scroll listbox user interface text layout console ncurses",
-    'platforms':"unix-like",
-    'description': "A full-featured console (xterm et al.) user interface library",
-    'long_description':open("README.rst").read().split('.. content-start\n',1)[1],
-    'classifiers':[
+    'name': "urwid",
+    'version': release,
+    'author': "Ian Ward",
+    'author_email': "ian@excess.org",
+    'ext_modules': [Extension('urwid.str_util', sources=['source/str_util.c'])],
+    'packages': ['urwid', 'urwid.tests'], 'url': "https://urwid.org/", 'license': "LGPL",
+    'keywords': "curses ui widget scroll listbox user interface text layout console ncurses",
+    'platforms': "unix-like", 'description': "A full-featured console (xterm et al.) user interface library",
+    'long_description': long_description, 'classifiers': [
         "Development Status :: 5 - Production/Stable",
         "Environment :: Console",
         "Environment :: Console :: Curses",
@@ -62,25 +57,23 @@
         "Operating System :: MacOS :: MacOS X",
         "Topic :: Software Development :: Libraries :: Python Modules",
         "Topic :: Software Development :: Widget Sets",
-        "Programming Language :: Python :: 2",
-        "Programming Language :: Python :: 2.7",
         "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.5",
-        "Programming Language :: Python :: 3.6",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
+        "Programming Language :: Python :: 3 :: Only",
         "Programming Language :: Python :: Implementation :: PyPy",
-        ],
-     }
-
-if have_setuptools:
-    setup_d['zip_safe'] = False
-    setup_d['test_suite'] = 'urwid.tests'
+    ],
+    'zip_safe': False,
+    'test_suite': 'urwid.tests',
+}
 
 if __name__ == "__main__":
     try:
         setup(**setup_d)
-    except (IOError, SystemExit) as e:
+    except (OSError, SystemExit) as e:
         import sys
         if "test" in sys.argv:
             raise
diff --git a/tox.ini b/tox.ini
index 014255063..96a18a785 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py3{6,7,8,9,10,11},pypy3
+envlist = py3{7,8,9,10,11},pypy3
 
 [testenv]
 usedevelop = true
@@ -12,3 +12,5 @@ deps =
 
 commands =
     coverage run -m unittest discover -s urwid -v
+[isort]
+profile = black
\ No newline at end of file
diff --git a/urwid/__init__.py b/urwid/__init__.py
index 3eb4e998c..a45e505b2 100644
--- a/urwid/__init__.py
+++ b/urwid/__init__.py
@@ -18,44 +18,157 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
+from __future__ import annotations
+
+from urwid.canvas import (
+    BlankCanvas,
+    Canvas,
+    CanvasCache,
+    CanvasCombine,
+    CanvasError,
+    CanvasJoin,
+    CanvasOverlay,
+    CompositeCanvas,
+    SolidCanvas,
+    TextCanvas,
+)
+from urwid.command_map import (
+    ACTIVATE,
+    CURSOR_DOWN,
+    CURSOR_LEFT,
+    CURSOR_MAX_LEFT,
+    CURSOR_MAX_RIGHT,
+    CURSOR_PAGE_DOWN,
+    CURSOR_PAGE_UP,
+    CURSOR_RIGHT,
+    CURSOR_UP,
+    REDRAW_SCREEN,
+    CommandMap,
+    command_map,
+)
+from urwid.container import (
+    Columns,
+    ColumnsError,
+    Frame,
+    FrameError,
+    GridFlow,
+    GridFlowError,
+    Overlay,
+    OverlayError,
+    Pile,
+    PileError,
+    WidgetContainerMixin,
+)
+from urwid.decoration import (
+    AttrMap,
+    AttrMapError,
+    AttrWrap,
+    BoxAdapter,
+    BoxAdapterError,
+    Filler,
+    FillerError,
+    Padding,
+    PaddingError,
+    WidgetDecoration,
+    WidgetDisable,
+    WidgetPlaceholder,
+)
+from urwid.font import (
+    Font,
+    HalfBlock5x4Font,
+    HalfBlock6x5Font,
+    HalfBlock7x7Font,
+    HalfBlockHeavy6x5Font,
+    Thin3x3Font,
+    Thin4x3Font,
+    Thin6x6Font,
+    get_all_fonts,
+)
+from urwid.graphics import (
+    BarGraph,
+    BarGraphError,
+    BarGraphMeta,
+    BigText,
+    GraphVScale,
+    LineBox,
+    ProgressBar,
+    scale_bar_values,
+)
+from urwid.listbox import (
+    ListBox,
+    ListBoxError,
+    ListWalker,
+    ListWalkerError,
+    SimpleFocusListWalker,
+    SimpleListWalker,
+)
+from urwid.main_loop import (
+    AsyncioEventLoop,
+    ExitMainLoop,
+    GLibEventLoop,
+    MainLoop,
+    SelectEventLoop,
+    TornadoEventLoop,
+)
+from urwid.monitored_list import MonitoredFocusList, MonitoredList
+from urwid.signals import (
+    MetaSignals,
+    Signals,
+    connect_signal,
+    disconnect_signal,
+    emit_signal,
+    register_signal,
+)
 from urwid.version import VERSION, __version__
-from urwid.widget import (FLOW, BOX, FIXED, LEFT, RIGHT, CENTER, TOP, MIDDLE,
-    BOTTOM, SPACE, ANY, CLIP, PACK, GIVEN, RELATIVE, RELATIVE_100, WEIGHT,
+from urwid.widget import (
+    ANY,
+    BOTTOM,
+    BOX,
+    CENTER,
+    CLIP,
+    FIXED,
+    FLOW,
+    GIVEN,
+    LEFT,
+    MIDDLE,
+    PACK,
+    RELATIVE,
+    RELATIVE_100,
+    RIGHT,
+    SPACE,
+    TOP,
+    WEIGHT,
+    BoxWidget,
+    Divider,
+    Edit,
+    EditError,
+    FixedWidget,
+    FlowWidget,
+    IntEdit,
+    SolidFill,
+    Text,
+    TextError,
+    Widget,
+    WidgetError,
     WidgetMeta,
-    WidgetError, Widget, FlowWidget, BoxWidget, fixed_size, FixedWidget,
-    Divider, SolidFill, TextError, Text, EditError, Edit, IntEdit,
-    delegate_to_widget_mixin, WidgetWrapError, WidgetWrap)
-from urwid.decoration import (WidgetDecoration, WidgetPlaceholder,
-    AttrMapError, AttrMap, AttrWrap, BoxAdapterError, BoxAdapter, PaddingError,
-    Padding, FillerError, Filler, WidgetDisable)
-from urwid.container import (GridFlowError, GridFlow, OverlayError, Overlay,
-    FrameError, Frame, PileError, Pile, ColumnsError, Columns,
-    WidgetContainerMixin)
-from urwid.wimp import (SelectableIcon, CheckBoxError, CheckBox, RadioButton,
-    Button, PopUpLauncher, PopUpTarget)
-from urwid.listbox import (ListWalkerError, ListWalker, SimpleListWalker,
-    SimpleFocusListWalker, ListBoxError, ListBox)
-from urwid.graphics import (BigText, LineBox, BarGraphMeta, BarGraphError,
-    BarGraph, GraphVScale, ProgressBar, scale_bar_values)
-from urwid.canvas import (CanvasCache, CanvasError, Canvas, TextCanvas,
-    BlankCanvas, SolidCanvas, CompositeCanvas, CanvasCombine, CanvasOverlay,
-    CanvasJoin)
-from urwid.font import (get_all_fonts, Font, Thin3x3Font, Thin4x3Font,
-    HalfBlock5x4Font, HalfBlock6x5Font, HalfBlockHeavy6x5Font, Thin6x6Font,
-    HalfBlock7x7Font)
-from urwid.signals import (MetaSignals, Signals, emit_signal, register_signal,
-    connect_signal, disconnect_signal)
-from urwid.monitored_list import MonitoredList, MonitoredFocusList
-from urwid.command_map import (CommandMap, command_map,
-    REDRAW_SCREEN, CURSOR_UP, CURSOR_DOWN, CURSOR_LEFT, CURSOR_RIGHT,
-    CURSOR_PAGE_UP, CURSOR_PAGE_DOWN, CURSOR_MAX_LEFT, CURSOR_MAX_RIGHT,
-    ACTIVATE)
-from urwid.main_loop import (ExitMainLoop, MainLoop, SelectEventLoop,
-    GLibEventLoop, TornadoEventLoop, AsyncioEventLoop)
+    WidgetWrap,
+    WidgetWrapError,
+    delegate_to_widget_mixin,
+    fixed_size,
+)
+from urwid.wimp import (
+    Button,
+    CheckBox,
+    CheckBoxError,
+    PopUpLauncher,
+    PopUpTarget,
+    RadioButton,
+    SelectableIcon,
+)
+
 try:
     from urwid.main_loop import TwistedEventLoop
 except ImportError:
@@ -64,21 +177,63 @@
     from urwid.main_loop import TrioEventLoop
 except ImportError:
     pass
-from urwid.text_layout import (TextLayout, StandardTextLayout, default_layout,
-    LayoutSegment)
-from urwid.display_common import (UPDATE_PALETTE_ENTRY, DEFAULT, BLACK,
-    DARK_RED, DARK_GREEN, BROWN, DARK_BLUE, DARK_MAGENTA, DARK_CYAN,
-    LIGHT_GRAY, DARK_GRAY, LIGHT_RED, LIGHT_GREEN, YELLOW, LIGHT_BLUE,
-    LIGHT_MAGENTA, LIGHT_CYAN, WHITE, AttrSpecError, AttrSpec, RealTerminal,
-    ScreenError, BaseScreen)
-from urwid.util import (calc_text_pos, calc_width, is_wide_char,
-    move_next_char, move_prev_char, within_double_byte, detected_encoding,
-    set_encoding, get_encoding_mode, apply_target_encoding, supports_unicode,
-    calc_trim_text, TagMarkupException, decompose_tagmarkup, MetaSuper,
-    int_scale, is_mouse_event)
-from urwid.treetools import (TreeWidgetError, TreeWidget, TreeNode,
-    ParentNode, TreeWalker, TreeListBox)
-from urwid.vterm import (TermModes, TermCharset, TermScroller, TermCanvas,
-    Terminal)
-
 from urwid import raw_display
+from urwid.display_common import (
+    BLACK,
+    BROWN,
+    DARK_BLUE,
+    DARK_CYAN,
+    DARK_GRAY,
+    DARK_GREEN,
+    DARK_MAGENTA,
+    DARK_RED,
+    DEFAULT,
+    LIGHT_BLUE,
+    LIGHT_CYAN,
+    LIGHT_GRAY,
+    LIGHT_GREEN,
+    LIGHT_MAGENTA,
+    LIGHT_RED,
+    UPDATE_PALETTE_ENTRY,
+    WHITE,
+    YELLOW,
+    AttrSpec,
+    AttrSpecError,
+    BaseScreen,
+    RealTerminal,
+    ScreenError,
+)
+from urwid.text_layout import (
+    LayoutSegment,
+    StandardTextLayout,
+    TextLayout,
+    default_layout,
+)
+from urwid.treetools import (
+    ParentNode,
+    TreeListBox,
+    TreeNode,
+    TreeWalker,
+    TreeWidget,
+    TreeWidgetError,
+)
+from urwid.util import (
+    MetaSuper,
+    TagMarkupException,
+    apply_target_encoding,
+    calc_text_pos,
+    calc_trim_text,
+    calc_width,
+    decompose_tagmarkup,
+    detected_encoding,
+    get_encoding_mode,
+    int_scale,
+    is_mouse_event,
+    is_wide_char,
+    move_next_char,
+    move_prev_char,
+    set_encoding,
+    supports_unicode,
+    within_double_byte,
+)
+from urwid.vterm import TermCanvas, TermCharset, Terminal, TermModes, TermScroller
diff --git a/urwid/_async_kw_event_loop.py b/urwid/_async_kw_event_loop.py
index ba8ad9d36..bc74b0b4c 100644
--- a/urwid/_async_kw_event_loop.py
+++ b/urwid/_async_kw_event_loop.py
@@ -18,7 +18,9 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
+
+from __future__ import annotations
 
 from .main_loop import EventLoop, ExitMainLoop
 
diff --git a/urwid/canvas.py b/urwid/canvas.py
index 479e88977..1f4789eb4 100644
--- a/urwid/canvas.py
+++ b/urwid/canvas.py
@@ -17,18 +17,27 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
+
+from __future__ import annotations
 
 import weakref
 
-from urwid.util import rle_len, rle_append_modify, rle_join_modify, rle_product, \
-    calc_width, calc_text_pos, apply_target_encoding, trim_text_attr_cs
-from urwid.text_layout import trim_line, LayoutSegment
+from urwid.text_layout import LayoutSegment, trim_line
+from urwid.util import (
+    apply_target_encoding,
+    calc_text_pos,
+    calc_width,
+    rle_append_modify,
+    rle_join_modify,
+    rle_len,
+    rle_product,
+    trim_text_attr_cs,
+)
 
 
-class CanvasCache(object):
+class CanvasCache:
     """
     Cache for rendered canvases.  Automatically populated and
     accessed by Widget render() MetaClass magic, cleared by
@@ -51,6 +60,7 @@ class CanvasCache(object):
     fetches = 0
     cleanups = 0
 
+    @classmethod
     def store(cls, wcls, canvas):
         """
         Store a weakref to canvas in the cache.
@@ -92,8 +102,8 @@ def walk_depends(canv):
         ref = weakref.ref(canvas, cls.cleanup)
         cls._refs[ref] = (widget, wcls, size, focus)
         cls._widgets.setdefault(widget, {})[(wcls, size, focus)] = ref
-    store = classmethod(store)
 
+    @classmethod
     def fetch(cls, widget, wcls, size, focus):
         """
         Return the cached canvas or None.
@@ -114,8 +124,8 @@ def fetch(cls, widget, wcls, size, focus):
         if canv:
             cls.hits += 1 # more stats
         return canv
-    fetch = classmethod(fetch)
 
+    @classmethod
     def invalidate(cls, widget):
         """
         Remove all canvases cached for widget.
@@ -138,8 +148,8 @@ def invalidate(cls, widget):
             pass
         for w in dependants:
             cls.invalidate(w)
-    invalidate = classmethod(invalidate)
 
+    @classmethod
     def cleanup(cls, ref):
         cls.cleanups += 1 # collect stats
 
@@ -161,8 +171,8 @@ def cleanup(cls, ref):
                 del cls._deps[widget]
             except KeyError:
                 pass
-    cleanup = classmethod(cleanup)
 
+    @classmethod
     def clear(cls):
         """
         Empty the cache.
@@ -170,14 +180,12 @@ def clear(cls):
         cls._widgets = {}
         cls._refs = {}
         cls._deps = {}
-    clear = classmethod(clear)
-
 
 
 class CanvasError(Exception):
     pass
 
-class Canvas(object):
+class Canvas:
     """
     base class for canvases
     """
@@ -233,7 +241,7 @@ def _text_content(self):
         Return the text content of the canvas as a list of strings,
         one for each row.
         """
-        return [bytes().join([text for (attr, cs, text) in row])
+        return [b''.join([text for (attr, cs, text) in row])
             for row in self.content()]
 
     text = property(_text_content, _raise_old_repr_error)
@@ -325,7 +333,7 @@ def __init__(self, text=None, attr=None, cs=None,
         check_width -- check and fix width of all lines in text
         """
         Canvas.__init__(self)
-        if text == None:
+        if text is None:
             text = []
 
         if check_width:
@@ -345,27 +353,27 @@ def __init__(self, text=None, attr=None, cs=None,
             else:
                 maxcol = 0
 
-        if attr == None:
+        if attr is None:
             attr = [[] for x in range(len(text))]
-        if cs == None:
+        if cs is None:
             cs = [[] for x in range(len(text))]
 
         # pad text and attr to maxcol
         for i in range(len(text)):
             w = widths[i]
             if w > maxcol:
-                raise CanvasError("Canvas text is wider than the maxcol specified \n%r\n%r\n%r"%(maxcol,widths,text))
+                raise CanvasError(f"Canvas text is wider than the maxcol specified \n{maxcol!r}\n{widths!r}\n{text!r}")
             if w < maxcol:
-                text[i] = text[i] + bytes().rjust(maxcol-w)
+                text[i] = text[i] + b''.rjust(maxcol-w)
             a_gap = len(text[i]) - rle_len( attr[i] )
             if a_gap < 0:
-                raise CanvasError("Attribute extends beyond text \n%r\n%r" % (text[i],attr[i]) )
+                raise CanvasError(f"Attribute extends beyond text \n{text[i]!r}\n{attr[i]!r}" )
             if a_gap:
                 rle_append_modify( attr[i], (None, a_gap))
 
             cs_gap = len(text[i]) - rle_len( cs[i] )
             if cs_gap < 0:
-                raise CanvasError("Character Set extends beyond text \n%r\n%r" % (text[i],cs[i]) )
+                raise CanvasError(f"Character Set extends beyond text \n{text[i]!r}\n{cs[i]!r}" )
             if cs_gap:
                 rle_append_modify( cs[i], (None, cs_gap))
 
@@ -469,7 +477,7 @@ def content(self, trim_left, trim_top, cols, rows, attr):
         def_attr = None
         if attr and None in attr:
             def_attr = attr[None]
-        line = [(def_attr, None, bytes().rjust(cols))]
+        line = [(def_attr, None, b''.rjust(cols))]
         for i in range(rows):
             yield line
 
@@ -492,7 +500,7 @@ class SolidCanvas(Canvas):
     def __init__(self, fill_char, cols, rows):
         Canvas.__init__(self)
         end, col = calc_text_pos(fill_char, 0, len(fill_char), 1)
-        assert col == 1, "Invalid fill_char: %r" % fill_char
+        assert col == 1, f"Invalid fill_char: {fill_char!r}"
         self._text, cs = apply_target_encoding(fill_char[:end])
         self._cs = cs[0][0]
         self.size = cols, rows
@@ -740,8 +748,8 @@ def overlay(self, other, left, top ):
         right = self.cols() - left - width
         bottom = self.rows() - top - height
 
-        assert right >= 0, "top canvas of overlay not the size expected!" + repr((other.cols(),left,right,width))
-        assert bottom >= 0, "top canvas of overlay not the size expected!" + repr((other.rows(),top,bottom,height))
+        assert right >= 0, f"top canvas of overlay not the size expected!{repr((other.cols(), left, right, width))}"
+        assert bottom >= 0, f"top canvas of overlay not the size expected!{repr((other.rows(), top, bottom, height))}"
 
         shards = self.shards
         top_shards = []
@@ -1300,14 +1308,14 @@ def attrrange( start_offs, end_offs, destw ):
                 rle_join_modify( linec, cs )
             elif s.offs:
                 if s.sc:
-                    line.append(bytes().rjust(s.sc))
+                    line.append(b''.rjust(s.sc))
                     attrrange( s.offs, s.offs, s.sc )
             else:
-                line.append(bytes().rjust(s.sc))
+                line.append(b''.rjust(s.sc))
                 linea.append((None, s.sc))
                 linec.append((None, s.sc))
 
-        t.append(bytes().join(line))
+        t.append(b''.join(line))
         a.append(linea)
         c.append(linec)
 
diff --git a/urwid/command_map.py b/urwid/command_map.py
index e6965f70d..08b0c8df9 100644
--- a/urwid/command_map.py
+++ b/urwid/command_map.py
@@ -17,9 +17,8 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
 REDRAW_SCREEN = 'redraw screen'
 CURSOR_UP = 'cursor up'
@@ -32,7 +31,7 @@
 CURSOR_MAX_RIGHT = 'cursor max right'
 ACTIVATE = 'activate'
 
-class CommandMap(object):
+class CommandMap:
     """
     dict-like object for looking up commands from keystrokes
 
diff --git a/urwid/compat.py b/urwid/compat.py
index 5734552c7..8bcbf0556 100644
--- a/urwid/compat.py
+++ b/urwid/compat.py
@@ -1,5 +1,4 @@
 #!/usr/bin/python
-# -*- coding: utf-8 -*-
 #
 # Urwid python compatibility definitions
 #    Copyright (C) 2011  Ian Ward
@@ -18,90 +17,20 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
-
-import sys
-
-
-PYTHON3 = sys.version_info > (3, 0)
-
-# for iterating over byte strings:
-# ord2 calls ord in python2 only
-# chr2 converts an ordinal value to a length-1 byte string
-# B returns a byte string in all supported python versions
-# bytes3 creates a byte string from a list of ordinal values
-if PYTHON3:
-    ord2 = lambda x: x
-    chr2 = lambda x: bytes([x])
-    B = lambda x: x.encode('iso8859-1')
-    bytes3 = bytes
-    text_type = str
-    xrange = range
-    text_types = (str,)
-
-    def reraise(tp, value, tb=None):
-        """
-        Reraise an exception.
-        Taken from "six" library (https://pythonhosted.org/six/).
-        """
-        try:
-            if value is None:
-                value = tp()
-            if value.__traceback__ is not tb:
-                raise value.with_traceback(tb)
-            raise value
-        finally:
-            value = None
-            tb = None
-else:
-    ord2 = ord
-    chr2 = chr
-    B = lambda x: x
-    bytes3 = lambda x: bytes().join([chr(c) for c in x])
-    text_type = unicode
-    xrange = xrange
-    text_types = (str, unicode)
 
+def reraise(tp, value, tb=None):
     """
     Reraise an exception.
     Taken from "six" library (https://pythonhosted.org/six/).
     """
-    def exec_(_code_, _globs_=None, _locs_=None):
-        """Execute code in a namespace."""
-        if _globs_ is None:
-            frame = sys._getframe(1)
-            _globs_ = frame.f_globals
-            if _locs_ is None:
-                _locs_ = frame.f_locals
-            del frame
-        elif _locs_ is None:
-            _locs_ = _globs_
-        exec("""exec _code_ in _globs_, _locs_""")
-
-    exec_("""def reraise(tp, value, tb=None):
     try:
-        raise tp, value, tb
+        if value is None:
+            value = tp()
+        if value.__traceback__ is not tb:
+            raise value.with_traceback(tb)
+        raise value
     finally:
+        value = None
         tb = None
-""")
-
-def with_metaclass(meta, *bases):
-    """
-    Create a base class with a metaclass.
-    Taken from "six" library (https://pythonhosted.org/six/).
-    """
-    # This requires a bit of explanation: the basic idea is to make a dummy
-    # metaclass for one level of class instantiation that replaces itself with
-    # the actual metaclass.
-    class metaclass(type):
-
-        def __new__(cls, name, this_bases, d):
-            return meta(name, bases, d)
-
-        @classmethod
-        def __prepare__(cls, name, this_bases):
-            return meta.__prepare__(name, bases)
-    return type.__new__(metaclass, 'temporary_class', (), {})
-
diff --git a/urwid/container.py b/urwid/container.py
index af52a8358..889c92e21 100755
--- a/urwid/container.py
+++ b/urwid/container.py
@@ -17,26 +17,57 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
+
+from __future__ import annotations
 
 from itertools import chain, repeat
-from urwid.compat import xrange
 
+from urwid.canvas import (
+    CanvasCombine,
+    CanvasJoin,
+    CanvasOverlay,
+    CompositeCanvas,
+    SolidCanvas,
+)
+from urwid.decoration import (
+    Filler,
+    Padding,
+    calculate_left_right_padding,
+    calculate_top_bottom_filler,
+    normalize_align,
+    normalize_height,
+    normalize_valign,
+    normalize_width,
+    simplify_align,
+    simplify_height,
+    simplify_valign,
+    simplify_width,
+)
+from urwid.monitored_list import MonitoredFocusList, MonitoredList
 from urwid.util import is_mouse_press
-from urwid.widget import (Widget, Divider, FLOW, FIXED, PACK, BOX, WidgetWrap,
-    GIVEN, WEIGHT, LEFT, RIGHT, RELATIVE, TOP, BOTTOM, CLIP, RELATIVE_100)
-from urwid.decoration import (Padding, Filler, calculate_left_right_padding,
-    calculate_top_bottom_filler, normalize_align, normalize_width,
-    normalize_valign, normalize_height, simplify_align, simplify_width,
-    simplify_valign, simplify_height)
-from urwid.monitored_list import MonitoredList, MonitoredFocusList
-from urwid.canvas import (CompositeCanvas, CanvasOverlay, CanvasCombine,
-    SolidCanvas, CanvasJoin)
-
-
-class WidgetContainerMixin(object):
+from urwid.widget import (
+    BOTTOM,
+    BOX,
+    CLIP,
+    FIXED,
+    FLOW,
+    GIVEN,
+    LEFT,
+    PACK,
+    RELATIVE,
+    RELATIVE_100,
+    RIGHT,
+    TOP,
+    WEIGHT,
+    Divider,
+    Widget,
+    WidgetWrap,
+)
+
+
+class WidgetContainerMixin:
     """
     Mixin class for widget containers implementing common container methods
     """
@@ -106,7 +137,7 @@ def get_focus_widgets(self):
                 return out
             out.append(w)
 
-class WidgetContainerListContentsMixin(object):
+class WidgetContainerListContentsMixin:
     """
     Mixin class for widget containers whose positions are indexes into
     a list available as self.contents.
@@ -116,14 +147,14 @@ def __iter__(self):
         Return an iterable of positions for this container from first
         to last.
         """
-        return iter(xrange(len(self.contents)))
+        return iter(range(len(self.contents)))
 
     def __reversed__(self):
         """
         Return an iterable of positions for this container from last
         to first.
         """
-        return iter(xrange(len(self.contents) - 1, -1, -1))
+        return iter(range(len(self.contents) - 1, -1, -1))
 
 
 class GridFlowError(Exception):
@@ -158,13 +189,13 @@ def __init__(self, cells, cell_width, h_sep, v_sep, align):
         self.v_sep = v_sep
         self.align = align
         self._cache_maxcol = None
-        self.__super.__init__(None)
+        super().__init__(None)
         # set self._w to something other than None
         self.get_display_widget(((h_sep+cell_width)*len(cells),))
 
     def _invalidate(self):
         self._cache_maxcol = None
-        self.__super._invalidate()
+        super()._invalidate()
 
     def _contents_modified(self, slc, new_items):
         for item in new_items:
@@ -173,7 +204,7 @@ def _contents_modified(self, slc, new_items):
                 if t != GIVEN:
                     raise ValueError
             except (TypeError, ValueError):
-                raise GridFlowError("added content invalid %r" % (item,))
+                raise GridFlowError(f"added content invalid {item!r}")
 
     def _get_cells(self):
         ml = MonitoredList(w for w, t in self.contents)
@@ -234,7 +265,7 @@ def options(self, width_type=GIVEN, width_amount=None):
         width_amount -- None to use the default cell_width for this GridFlow
         """
         if width_type != GIVEN:
-            raise GridFlowError("invalid width_type: %r" % (width_type,))
+            raise GridFlowError(f"invalid width_type: {width_type!r}")
         if width_amount is None:
             width_amount = self._cell_width
         return (width_type, width_amount)
@@ -271,7 +302,7 @@ def _set_focus_cell(self, cell):
             if cell == w:
                 self.focus_position = i
                 return
-        raise ValueError("Widget not found in GridFlow contents: %r" % (cell,))
+        raise ValueError(f"Widget not found in GridFlow contents: {cell!r}")
     focus_cell = property(get_focus, _set_focus_cell, doc="""
         The widget in focus, for backwards compatibility.
 
@@ -299,7 +330,7 @@ def _set_focus_position(self, position):
             if position < 0 or position >= len(self.contents):
                 raise IndexError
         except (TypeError, IndexError):
-            raise IndexError("No GridFlow child widget at position %s" % (position,))
+            raise IndexError(f"No GridFlow child widget at position {position}")
         self.contents.focus = position
     focus_position = property(_get_focus_position, _set_focus_position, doc="""
         index of child widget in focus. Raises :exc:`IndexError` if read when
@@ -407,41 +438,41 @@ def keypress(self, size, key):
         Captures focus changes.
         """
         self.get_display_widget(size)
-        key = self.__super.keypress(size, key)
+        key = super().keypress(size, key)
         if key is None:
             self._set_focus_from_display_widget()
         return key
 
     def rows(self, size, focus=False):
         self.get_display_widget(size)
-        return self.__super.rows(size, focus=focus)
+        return super().rows(size, focus=focus)
 
     def render(self, size, focus=False ):
         self.get_display_widget(size)
-        return self.__super.render(size, focus)
+        return super().render(size, focus)
 
     def get_cursor_coords(self, size):
         """Get cursor from display widget."""
         self.get_display_widget(size)
-        return self.__super.get_cursor_coords(size)
+        return super().get_cursor_coords(size)
 
     def move_cursor_to_coords(self, size, col, row):
         """Set the widget in focus based on the col + row."""
         self.get_display_widget(size)
-        rval = self.__super.move_cursor_to_coords(size, col, row)
+        rval = super().move_cursor_to_coords(size, col, row)
         self._set_focus_from_display_widget()
         return rval
 
     def mouse_event(self, size, event, button, col, row, focus):
         self.get_display_widget(size)
-        self.__super.mouse_event(size, event, button, col, row, focus)
+        super().mouse_event(size, event, button, col, row, focus)
         self._set_focus_from_display_widget()
         return True # at a minimum we adjusted our focus
 
     def get_pref_col(self, size):
         """Return pref col from display widget."""
         self.get_display_widget(size)
-        return self.__super.get_pref_col(size)
+        return super().get_pref_col(size)
 
 
 
@@ -507,7 +538,7 @@ def __init__(self, top_w, bottom_w, align, width, valign, height,
         widgets when determining the size and position of *top_w*. *bottom_w* is
         always rendered the full size available "below" *top_w*.
         """
-        self.__super.__init__()
+        super().__init__()
 
         self.top_w = top_w
         self.bottom_w = bottom_w
@@ -624,7 +655,7 @@ def _set_focus_position(self, position):
         doc="index of child widget in focus, currently always 1")
 
     def _contents(self):
-        class OverlayContents(object):
+        class OverlayContents:
             def __len__(inner_self):
                 return 2
             __getitem__ = self._contents__getitem__
@@ -641,13 +672,12 @@ def _contents__getitem__(self, index):
                 self.right, self.valign_type, self.valign_amount,
                 self.height_type, self.height_amount,
                 self.min_height, self.top, self.bottom))
-        raise IndexError("Overlay.contents has no position %r"
-            % (index,))
+        raise IndexError(f"Overlay.contents has no position {index!r}")
     def _contents__setitem__(self, index, value):
         try:
             value_w, value_options = value
         except (ValueError, TypeError):
-            raise OverlayError("added content invalid: %r" % (value,))
+            raise OverlayError(f"added content invalid: {value!r}")
         if index == 0:
             if value_options != self._DEFAULT_BOTTOM_OPTIONS:
                 raise OverlayError("bottom_options must be set to "
@@ -660,8 +690,7 @@ def _contents__setitem__(self, index, value):
                     height_type, height_amount, min_height, top, bottom,
                     ) = value_options
             except (ValueError, TypeError):
-                raise OverlayError("top_options is invalid: %r"
-                    % (value_options,))
+                raise OverlayError(f"top_options is invalid: {value_options!r}")
             # normalize first, this is where errors are raised
             align_type, align_amount = normalize_align(
                 simplify_align(align_type, align_amount), OverlayError)
@@ -686,8 +715,7 @@ def _contents__setitem__(self, index, value):
             self.min_width = min_width
             self.min_height = min_height
         else:
-            raise IndexError("Overlay.contents has no position %r"
-                % (index,))
+            raise IndexError(f"Overlay.contents has no position {index!r}")
         self._invalidate()
     contents = property(_contents, doc="""
         a list-like object similar to::
@@ -839,7 +867,7 @@ def __init__(self, body, header=None, footer=None, focus_part='body'):
         :param focus_part:  'header', 'footer' or 'body'
         :type focus_part: str
         """
-        self.__super.__init__()
+        super().__init__()
 
         self._header = header
         self._body = body
@@ -882,10 +910,10 @@ def set_focus(self, part):
         :type part: str
         """
         if part not in ('header', 'footer', 'body'):
-            raise IndexError('Invalid position for Frame: %s' % (part,))
+            raise IndexError(f'Invalid position for Frame: {part}')
         if (part == 'header' and self._header is None) or (
                 part == 'footer' and self._footer is None):
-            raise IndexError('This Frame has no %s' % (part,))
+            raise IndexError(f'This Frame has no {part}')
         self.focus_part = part
         self._invalidate()
 
@@ -917,7 +945,7 @@ def _get_focus(self):
         """)
 
     def _contents(self):
-        class FrameContents(object):
+        class FrameContents:
             def __len__(inner_self):
                 return len(inner_self.keys())
             def items(inner_self):
@@ -954,16 +982,16 @@ def _contents__getitem__(self, key):
             return (self._header, None)
         if key == 'footer' and self._footer:
             return (self._footer, None)
-        raise KeyError("Frame.contents has no key: %r" % (key,))
+        raise KeyError(f"Frame.contents has no key: {key!r}")
     def _contents__setitem__(self, key, value):
         if key not in ('body', 'header', 'footer'):
-            raise KeyError("Frame.contents has no key: %r" % (key,))
+            raise KeyError(f"Frame.contents has no key: {key!r}")
         try:
             value_w, value_options = value
             if value_options is not None:
                 raise ValueError
         except (ValueError, TypeError):
-            raise FrameError("added content invalid: %r" % (value,))
+            raise FrameError(f"added content invalid: {value!r}")
         if key == 'body':
             self.body = value_w
         elif key == 'footer':
@@ -972,10 +1000,10 @@ def _contents__setitem__(self, key, value):
             self.header = value_w
     def _contents__delitem__(self, key):
         if key not in ('header', 'footer'):
-            raise KeyError("Frame.contents can't remove key: %r" % (key,))
+            raise KeyError(f"Frame.contents can't remove key: {key!r}")
         if (key == 'header' and self._header is None
                 ) or (key == 'footer' and self._footer is None):
-            raise KeyError("Frame.contents has no key: %r" % (key,))
+            raise KeyError(f"Frame.contents has no key: {key!r}")
         if key == 'header':
             self.header = None
         else:
@@ -1259,7 +1287,7 @@ def __init__(self, widget_list, focus_item=None):
             one ``'weight'`` tuple in :attr:`widget_list`.
         """
         self._selectable = False
-        self.__super.__init__()
+        super().__init__()
         self._contents = MonitoredFocusList()
         self._contents.set_modified_callback(self._contents_modified)
         self._contents.set_focus_changed_callback(lambda f: self._invalidate())
@@ -1284,7 +1312,7 @@ def __init__(self, widget_list, focus_item=None):
                 self.contents.append((w, (f, height)))
             else:
                 raise PileError(
-                    "initial widget list item invalid %r" % (original,))
+                    f"initial widget list item invalid {original!r}")
             if focus_item is None and w.selectable():
                 focus_item = i
 
@@ -1308,7 +1336,7 @@ def _validate_contents_modified(self, slc, new_items):
                 if t not in (PACK, GIVEN, WEIGHT):
                     raise ValueError
             except (TypeError, ValueError):
-                raise PileError("added content invalid: %r" % (item,))
+                raise PileError(f"added content invalid: {item!r}")
 
     def _get_widget_list(self):
         ml = MonitoredList(w for w, t in self.contents)
@@ -1399,7 +1427,7 @@ def options(height_type=WEIGHT, height_amount=1):
         if height_type == PACK:
             return (PACK, None)
         if height_type not in (GIVEN, WEIGHT):
-            raise PileError('invalid height_type: %r' % (height_type,))
+            raise PileError(f'invalid height_type: {height_type!r}')
         return (height_type, height_amount)
 
     def set_focus(self, item):
@@ -1419,7 +1447,7 @@ def set_focus(self, item):
             if item == w:
                 self.focus_position = i
                 return
-        raise ValueError("Widget not found in Pile contents: %r" % (item,))
+        raise ValueError(f"Widget not found in Pile contents: {item!r}")
 
     def get_focus(self):
         """
@@ -1462,7 +1490,7 @@ def _set_focus_position(self, position):
             if position < 0 or position >= len(self.contents):
                 raise IndexError
         except (TypeError, IndexError):
-            raise IndexError("No Pile child widget at position %s" % (position,))
+            raise IndexError(f"No Pile child widget at position {position}")
         self.contents.focus = position
     focus_position = property(_get_focus_position, _set_focus_position, doc="""
         index of child widget in focus. Raises :exc:`IndexError` if read when
@@ -1773,7 +1801,7 @@ def __init__(self, widget_list, dividechars=0, focus_column=None,
         filling the full height.
         """
         self._selectable = False
-        self.__super.__init__()
+        super().__init__()
         self._contents = MonitoredFocusList()
         self._contents.set_modified_callback(self._contents_modified)
         self._contents.set_focus_changed_callback(lambda f: self._invalidate())
@@ -1801,7 +1829,7 @@ def __init__(self, widget_list, dividechars=0, focus_column=None,
                 self.contents.append((w, (f, width, i in box_columns)))
             else:
                 raise ColumnsError(
-                    "initial widget list item invalid: %r" % (original,))
+                    f"initial widget list item invalid: {original!r}")
             if focus_column is None and w.selectable():
                 focus_column = i
 
@@ -1828,7 +1856,7 @@ def _validate_contents_modified(self, slc, new_items):
                 if t not in (PACK, GIVEN, WEIGHT):
                     raise ValueError
             except (TypeError, ValueError):
-                raise ColumnsError("added content invalid %r" % (item,))
+                raise ColumnsError(f"added content invalid {item!r}")
 
     def _get_widget_list(self):
         ml = MonitoredList(w for w, t in self.contents)
@@ -1945,12 +1973,12 @@ def options(width_type=WEIGHT, width_amount=1, box_widget=False):
         if width_type == PACK:
             width_amount = None
         if width_type not in (PACK, GIVEN, WEIGHT):
-            raise ColumnsError('invalid width_type: %r' % (width_type,))
+            raise ColumnsError(f'invalid width_type: {width_type!r}')
         return (width_type, width_amount, box_widget)
 
     def _invalidate(self):
         self._cache_maxcol = None
-        self.__super._invalidate()
+        super()._invalidate()
 
     def set_focus_column(self, num):
         """
@@ -1987,7 +2015,7 @@ def set_focus(self, item):
             if item == w:
                 self.focus_position = i
                 return
-        raise ValueError("Widget not found in Columns contents: %r" % (item,))
+        raise ValueError(f"Widget not found in Columns contents: {item!r}")
 
     def get_focus(self):
         """
@@ -2019,7 +2047,7 @@ def _set_focus_position(self, position):
             if position < 0 or position >= len(self.contents):
                 raise IndexError
         except (TypeError, IndexError):
-            raise IndexError("No Columns child widget at position %s" % (position,))
+            raise IndexError(f"No Columns child widget at position {position}")
         self.contents.focus = position
     focus_position = property(_get_focus_position, _set_focus_position, doc="""
         index of child widget in focus. Raises :exc:`IndexError` if read when
diff --git a/urwid/curses_display.py b/urwid/curses_display.py
index c37507c5c..3a79ec760 100755
--- a/urwid/curses_display.py
+++ b/urwid/curses_display.py
@@ -17,22 +17,26 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
 """
 Curses-based UI implementation
 """
 
+from __future__ import annotations
+
 import curses
+
 import _curses
 
 from urwid import escape
-
-from urwid.display_common import BaseScreen, RealTerminal, AttrSpec, \
-    UNPRINTABLE_TRANS_TABLE
-from urwid.compat import PYTHON3, text_type, xrange, ord2
+from urwid.display_common import (
+    UNPRINTABLE_TRANS_TABLE,
+    AttrSpec,
+    BaseScreen,
+    RealTerminal,
+)
 
 KEY_RESIZE = 410 # curses.KEY_RESIZE (sometimes not defined)
 KEY_MOUSE = 409 # curses.KEY_MOUSE
@@ -60,7 +64,7 @@
 
 class Screen(BaseScreen, RealTerminal):
     def __init__(self):
-        super(Screen,self).__init__()
+        super().__init__()
         self.curses_pairs = [
             (None,None), # Can't be sure what pair 0 will default to
         ]
@@ -130,7 +134,7 @@ def _start(self):
         if not self._signal_keys_set:
             self._old_signal_keys = self.tty_signal_keys()
 
-        super(Screen, self)._start()
+        super()._start()
 
     def _stop(self):
         """
@@ -146,7 +150,7 @@ def _stop(self):
         if self._old_signal_keys:
             self.tty_signal_keys(*self._old_signal_keys)
 
-        super(Screen, self)._stop()
+        super()._stop()
 
 
     def _setup_colour_pairs(self):
@@ -159,8 +163,8 @@ def _setup_colour_pairs(self):
         if not self.has_color:
             return
 
-        for fg in xrange(8):
-            for bg in xrange(8):
+        for fg in range(8):
+            for bg in range(8):
                 # leave out white on black
                 if fg == curses.COLOR_WHITE and \
                    bg == curses.COLOR_BLACK:
@@ -368,7 +372,7 @@ def _encode_mouse_event(self):
         l = []
         def append_button( b ):
             b |= mod
-            l.extend([ 27, ord2('['), ord2('M'), b+32, x+33, y+33 ])
+            l.extend([ 27, '[', 'M', b+32, x+33, y+33 ])
 
         if bstate & curses.BUTTON1_PRESSED and last & 1 == 0:
             append_button( 0 )
@@ -515,14 +519,11 @@ def draw_screen(self, size, r ):
                 try:
                     if cs in ("0", "U"):
                         for i in range(len(seg)):
-                            self.s.addch( 0x400000 + ord2(seg[i]) )
+                            self.s.addch( 0x400000 + seg[i] )
                     else:
                         assert cs is None
-                        if PYTHON3:
-                            assert isinstance(seg, bytes)
-                            self.s.addstr(seg.decode('utf-8'))
-                        else:
-                            self.s.addstr(seg)
+                        assert isinstance(seg, bytes)
+                        self.s.addstr(seg.decode('utf-8'))
                 except _curses.error:
                     # it's ok to get out of the
                     # screen on the lower right
@@ -561,20 +562,20 @@ def clear(self):
 class _test:
     def __init__(self):
         self.ui = Screen()
-        self.l = list(_curses_colours.keys())
-        self.l.sort()
+        self.l = sorted(_curses_colours)
+
         for c in self.l:
             self.ui.register_palette( [
-                (c+" on black", c, 'black', 'underline'),
-                (c+" on dark blue",c, 'dark blue', 'bold'),
-                (c+" on light gray",c,'light gray', 'standout'),
+                (f"{c} on black", c, 'black', 'underline'),
+                (f"{c} on dark blue",c, 'dark blue', 'bold'),
+                (f"{c} on light gray",c,'light gray', 'standout'),
                 ])
         self.ui.run_wrapper(self.run)
 
     def run(self):
         class FakeRender: pass
         r = FakeRender()
-        text = ["  has_color = "+repr(self.ui.has_color),""]
+        text = [f"  has_color = {self.ui.has_color!r}",""]
         attr = [[],[]]
         r.coords = {}
         r.cursor = None
@@ -582,7 +583,7 @@ class FakeRender: pass
         for c in self.l:
             t = ""
             a = []
-            for p in c+" on black",c+" on dark blue",c+" on light gray":
+            for p in f"{c} on black",f"{c} on dark blue",f"{c} on light gray":
 
                 a.append((p,27))
                 t=t+ (p+27*" ")[:27]
@@ -605,12 +606,12 @@ class FakeRender: pass
             t = ""
             a = []
             for k in keys:
-                if type(k) == text_type: k = k.encode("utf-8")
-                t += "'"+k + "' "
+                if type(k) == str: k = k.encode("utf-8")
+                t += f"'{k}' "
                 a += [(None,1), ('yellow on dark blue',len(k)),
                     (None,2)]
 
-            text.append(t + ": "+ repr(raw))
+            text.append(f"{t}: {raw!r}")
             attr.append(a)
             text = text[-rows:]
             attr = attr[-rows:]
diff --git a/urwid/decoration.py b/urwid/decoration.py
index 9c1802809..28096ab8f 100755
--- a/urwid/decoration.py
+++ b/urwid/decoration.py
@@ -17,17 +17,36 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
-from urwid.util import int_scale
-from urwid.widget import (Widget, WidgetError,
-    BOX, FLOW, LEFT, CENTER, RIGHT, PACK, CLIP, GIVEN, RELATIVE, RELATIVE_100,
-    TOP, MIDDLE, BOTTOM, delegate_to_widget_mixin)
-from urwid.split_repr import remove_defaults
+from __future__ import annotations
+
 from urwid.canvas import CompositeCanvas, SolidCanvas
-from urwid.widget import Divider, Edit, Text, SolidFill # doctests
+from urwid.split_repr import remove_defaults
+from urwid.util import int_scale
+from urwid.widget import (  # doctests
+    BOTTOM,
+    BOX,
+    CENTER,
+    CLIP,
+    FLOW,
+    GIVEN,
+    LEFT,
+    MIDDLE,
+    PACK,
+    RELATIVE,
+    RELATIVE_100,
+    RIGHT,
+    TOP,
+    Divider,
+    Edit,
+    SolidFill,
+    Text,
+    Widget,
+    WidgetError,
+    delegate_to_widget_mixin,
+)
 
 
 class WidgetDecoration(Widget):  # "decorator" was already taken
@@ -49,7 +68,7 @@ class WidgetDecoration(Widget):  # "decorator" was already taken
     def __init__(self, original_widget):
         self._original_widget = original_widget
     def _repr_words(self):
-        return self.__super._repr_words() + [repr(self._original_widget)]
+        return super()._repr_words() + [repr(self._original_widget)]
 
     def _get_original_widget(self):
         return self._original_widget
@@ -140,7 +159,7 @@ def __init__(self, w, attr_map, focus_map=None):
         >>> next(am2.render(size).content())
         [('greeting', None, ...'hi'), ('bg', None, ...'   ')]
         """
-        self.__super.__init__(w)
+        super().__init__(w)
 
         if type(attr_map) != dict:
             self.set_attr_map({None: attr_map})
@@ -154,7 +173,7 @@ def __init__(self, w, attr_map, focus_map=None):
 
     def _repr_attrs(self):
         # only include the focus_attr when it takes effect (not None)
-        d = dict(self.__super._repr_attrs(), attr_map=self._attr_map)
+        d = dict(super()._repr_attrs(), attr_map=self._attr_map)
         if self._focus_map is not None:
             d['focus_map'] = self._focus_map
         return d
@@ -253,11 +272,11 @@ def __init__(self, w, attr, focus_attr=None):
         >>> next(aw.render(size, focus=True).content())
         [('fgreet', None, ...'hi   ')]
         """
-        self.__super.__init__(w, attr, focus_attr)
+        super().__init__(w, attr, focus_attr)
 
     def _repr_attrs(self):
         # only include the focus_attr when it takes effect (not None)
-        d = dict(self.__super._repr_attrs(), attr=self.attr)
+        d = dict(super()._repr_attrs(), attr=self.attr)
         del d['attr_map']
         if 'focus_map' in d:
             del d['focus_map']
@@ -342,21 +361,20 @@ def __init__(self, box_widget, height):
         <BoxAdapter flow widget <SolidFill box widget 'x'> height=5>
         """
         if hasattr(box_widget, 'sizing') and BOX not in box_widget.sizing():
-            raise BoxAdapterError("%r is not a box widget" %
-                box_widget)
+            raise BoxAdapterError(f"{box_widget!r} is not a box widget")
         WidgetDecoration.__init__(self,box_widget)
 
         self.height = height
 
     def _repr_attrs(self):
-        return dict(self.__super._repr_attrs(), height=self.height)
+        return dict(super()._repr_attrs(), height=self.height)
 
     # originally stored as box_widget, keep for compatibility
     box_widget = property(WidgetDecoration._get_original_widget,
         WidgetDecoration._set_original_widget)
 
     def sizing(self):
-        return set([FLOW])
+        return {FLOW}
 
     def rows(self, size, focus=False):
         """
@@ -482,7 +500,7 @@ def __init__(self, w, align=LEFT, width=RELATIVE_100, min_width=None,
         |  hi   |
         |  there|
         """
-        self.__super.__init__(w)
+        super().__init__(w)
 
         # convert obsolete parameters 'fixed left' and 'fixed right':
         if type(align) == tuple and align[0] in ('fixed left',
@@ -515,11 +533,11 @@ def __init__(self, w, align=LEFT, width=RELATIVE_100, min_width=None,
 
     def sizing(self):
         if self._width_type == CLIP:
-            return set([FLOW])
+            return {FLOW}
         return self.original_widget.sizing()
 
     def _repr_attrs(self):
-        attrs = dict(self.__super._repr_attrs(),
+        attrs = dict(super()._repr_attrs(),
             align=self.align,
             width=self.width,
             left=self.left,
@@ -726,7 +744,7 @@ def __init__(self, body, valign=MIDDLE, height=PACK, min_height=None,
         reducing the valign amount when necessary.  If height still
         cannot be satisfied it will also be reduced.
         """
-        self.__super.__init__(body)
+        super().__init__(body)
 
         # convert old parameters to the new top/bottom values
         if isinstance(height, tuple):
@@ -767,10 +785,10 @@ def __init__(self, body, valign=MIDDLE, height=PACK, min_height=None,
             self.min_height = None
 
     def sizing(self):
-        return set([BOX]) # always a box widget
+        return {BOX} # always a box widget
 
     def _repr_attrs(self):
-        attrs = dict(self.__super._repr_attrs(),
+        attrs = dict(super()._repr_attrs(),
             valign=simplify_valign(self.valign_type, self.valign_amount),
             height=simplify_height(self.height_type, self.height_amount),
             top=self.top,
diff --git a/urwid/display_common.py b/urwid/display_common.py
index 8361b453d..b135a5f96 100755
--- a/urwid/display_common.py
+++ b/urwid/display_common.py
@@ -16,24 +16,25 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
+
+from __future__ import annotations
 
 import os
 import sys
+import warnings
 
 try:
     import termios
 except ImportError:
     pass # windows
 
-from urwid.util import StoppingContext, int_scale
 from urwid import signals
-from urwid.compat import B, bytes3, xrange, with_metaclass
+from urwid.util import StoppingContext, int_scale
 
 # for replacing unprintable bytes with '?'
-UNPRINTABLE_TRANS_TABLE = B("?") * 32 + bytes3(list(xrange(32,256)))
+UNPRINTABLE_TRANS_TABLE = b"?" * 32 + bytes(list(range(32,256)))
 
 
 # signals sent by BaseScreen
@@ -234,7 +235,7 @@ def _gray_num_88(gnum):
 
 def _color_desc_true(num):
 
-    return "#%06x" %(num)
+    return f"#{num:06x}"
 
 def _color_desc_256(num):
     """
@@ -314,7 +315,7 @@ def _parse_color_true(desc):
         h = desc[1:]
         return int(h, 16)
     elif len(desc) == 4:
-        h = "0x%s0%s0%s" %(desc[1], desc[2], desc[3])
+        h = f"0x{desc[1]}0{desc[2]}0{desc[3]}"
         return int(h, 16)
     return None
 
@@ -474,7 +475,7 @@ def _parse_color_88(desc):
 class AttrSpecError(Exception):
     pass
 
-class AttrSpec(object):
+class AttrSpec:
     def __init__(self, fg, bg, colors=256):
         """
         fg -- a string containing a comma-separated foreground color
@@ -537,25 +538,66 @@ def __init__(self, fg, bg, colors=256):
         self.foreground = fg
         self.background = bg
         if self.colors > colors:
-            raise AttrSpecError(('foreground/background (%s/%s) require ' +
-                'more colors than have been specified (%d).') %
-                (repr(fg), repr(bg), colors))
-
-    foreground_basic = property(lambda s: s._value & _FG_BASIC_COLOR != 0)
-    foreground_high = property(lambda s: s._value & _FG_HIGH_COLOR != 0)
-    foreground_true = property(lambda s: s._value & _FG_TRUE_COLOR != 0)
-    foreground_number = property(lambda s: s._value & _FG_COLOR_MASK)
-    background_basic = property(lambda s: s._value & _BG_BASIC_COLOR != 0)
-    background_high = property(lambda s: s._value & _BG_HIGH_COLOR != 0)
-    background_true = property(lambda s: s._value & _BG_TRUE_COLOR != 0)
-    background_number = property(lambda s: (s._value & _BG_COLOR_MASK)
-        >> _BG_SHIFT)
-    italics = property(lambda s: s._value & _ITALICS != 0)
-    bold = property(lambda s: s._value & _BOLD != 0)
-    underline = property(lambda s: s._value & _UNDERLINE != 0)
-    blink = property(lambda s: s._value & _BLINK != 0)
-    standout = property(lambda s: s._value & _STANDOUT != 0)
-    strikethrough = property(lambda s: s._value & _STRIKETHROUGH != 0)
+            raise AttrSpecError(
+                f'foreground/background ({fg!r}/{bg!r}) '
+                f'require more colors than have been specified ({colors:d}).'
+            )
+
+    @property
+    def foreground_basic(self):
+        return self._value & _FG_BASIC_COLOR != 0
+
+    @property
+    def foreground_high(self):
+        return self._value & _FG_HIGH_COLOR != 0
+
+    @property
+    def foreground_true(self):
+        return self._value & _FG_TRUE_COLOR != 0
+
+    @property
+    def foreground_number(self):
+        return self._value & _FG_COLOR_MASK
+
+    @property
+    def background_basic(self):
+        return self._value & _BG_BASIC_COLOR != 0
+
+    @property
+    def background_high(self):
+        return self._value & _BG_HIGH_COLOR != 0
+
+    @property
+    def background_true(self):
+        return self._value & _BG_TRUE_COLOR != 0
+
+    @property
+    def background_number(self):
+        return (self._value & _BG_COLOR_MASK) >> _BG_SHIFT
+
+    @property
+    def italics(self):
+        return self._value & _ITALICS != 0
+
+    @property
+    def bold(self):
+        return self._value & _BOLD != 0
+
+    @property
+    def underline(self):
+        return self._value & _UNDERLINE != 0
+
+    @property
+    def blink(self):
+        return self._value & _BLINK != 0
+
+    @property
+    def standout(self):
+        return self._value & _STANDOUT != 0
+
+    @property
+    def strikethrough(self):
+        return self._value & _STRIKETHROUGH != 0
 
     def _colors(self):
         """
@@ -569,7 +611,7 @@ def _colors(self):
             return 256
         if self._value & (_BG_TRUE_COLOR | _FG_TRUE_COLOR):
             return 2**24
-        if self._value & (_BG_BASIC_COLOR | _BG_BASIC_COLOR):
+        if self._value & (_BG_BASIC_COLOR | _FG_BASIC_COLOR):
             return 16
         return 1
     colors = property(_colors)
@@ -579,11 +621,11 @@ def __repr__(self):
         Return an executable python representation of the AttrSpec
         object.
         """
-        args = "%r, %r" % (self.foreground, self.background)
+        args = f"{self.foreground!r}, {self.background!r}"
         if self.colors == 88:
             # 88-color mode is the only one that is handled differently
-            args = args + ", colors=88"
-        return "%s(%s)" % (self.__class__.__name__, args)
+            args = f"{args}, colors=88"
+        return f"{self.__class__.__name__}({args})"
 
     def _foreground_color(self):
         """Return only the color component of the foreground."""
@@ -612,9 +654,8 @@ def _set_foreground(self, foreground):
             if part in _ATTRIBUTES:
                 # parse and store "settings"/attributes in flags
                 if flags & _ATTRIBUTES[part]:
-                    raise AttrSpecError(("Setting %s specified more than" +
-                        "once in foreground (%s)") % (repr(part),
-                        repr(foreground)))
+                    raise AttrSpecError(
+                        f"Setting {part!r} specified more than once in foreground ({foreground!r})")
                 flags |= _ATTRIBUTES[part]
                 continue
             # past this point we must be specifying a color
@@ -634,11 +675,9 @@ def _set_foreground(self, foreground):
                 flags |= _FG_HIGH_COLOR
             # _parse_color_*() return None for unrecognised colors
             if scolor is None:
-                raise AttrSpecError(("Unrecognised color specification %s " +
-                    "in foreground (%s)") % (repr(part), repr(foreground)))
+                raise AttrSpecError(f"Unrecognised color specification {part!r} in foreground ({foreground!r})")
             if color is not None:
-                raise AttrSpecError(("More than one color given for " +
-                    "foreground (%s)") % (repr(foreground),))
+                raise AttrSpecError(f"More than one color given for foreground ({foreground!r})")
             color = scolor
         if color is None:
             color = 0
@@ -675,8 +714,7 @@ def _set_background(self, background):
             color = _parse_color_256(_true_to_256(background) or background)
             flags |= _BG_HIGH_COLOR
         if color is None:
-            raise AttrSpecError(("Unrecognised color specification " +
-                "in background (%s)") % (repr(background),))
+            raise AttrSpecError(f"Unrecognised color specification in background ({background!r})")
         self._value = (self._value & ~_BG_MASK) | (color << _BG_SHIFT) | flags
 
     background = property(_background, _set_background)
@@ -701,7 +739,7 @@ def get_rgb_values(self):
             assert self.foreground_number < 88, "Invalid AttrSpec _value"
             vals = _COLOR_VALUES_88[self.foreground_number]
         elif self.colors == 2**24:
-            h = "%06x" %(self.foreground_number)
+            h = f"{self.foreground_number:06x}"
             vals = tuple([int(x, 16) for x in [h[0:2], h[2:4], h[4:6]]])
         else:
             vals = _COLOR_VALUES_256[self.foreground_number]
@@ -712,7 +750,7 @@ def get_rgb_values(self):
             assert self.background_number < 88, "Invalid AttrSpec _value"
             return vals + _COLOR_VALUES_88[self.background_number]
         elif self.colors == 2**24:
-            h = "%06x" %(self.background_number)
+            h = f"{self.background_number:06x}"
             return vals + tuple([int(x, 16) for x in [h[0:2], h[2:4], h[4:6]]])
         else:
             return vals + _COLOR_VALUES_256[self.background_number]
@@ -726,9 +764,9 @@ def __ne__(self, other):
     __hash__ = object.__hash__
 
 
-class RealTerminal(object):
+class RealTerminal:
     def __init__(self):
-        super(RealTerminal,self).__init__()
+        super().__init__()
         self._signal_keys_set = False
         self._old_signal_keys = None
 
@@ -782,18 +820,20 @@ def tty_signal_keys(self, intr=None, quit=None, start=None,
 class ScreenError(Exception):
     pass
 
-class BaseScreen(with_metaclass(signals.MetaSignals, object)):
+class BaseScreen(metaclass=signals.MetaSignals):
     """
     Base class for Screen classes (raw_display.Screen, .. etc)
     """
     signals = [UPDATE_PALETTE_ENTRY, INPUT_DESCRIPTORS_CHANGED]
 
     def __init__(self):
-        super(BaseScreen,self).__init__()
+        super().__init__()
         self._palette = {}
         self._started = False
 
-    started = property(lambda self: self._started)
+    @property
+    def started(self):
+        return self._started
 
     def start(self, *args, **kwargs):
         """Set up the screen.  If the screen has already been started, does
@@ -830,10 +870,13 @@ def run_wrapper(self, fn, *args, **kwargs):
 
         Deprecated in favor of calling `start` as a context manager.
         """
+        warnings.warn(
+            "run_wrapper is deprecated in favor of calling `start` as a context manager.",
+            DeprecationWarning,
+        )
         with self.start(*args, **kwargs):
             return fn()
 
-
     def register_palette(self, palette):
         """Register a set of palette entries.
 
@@ -856,11 +899,10 @@ def register_palette(self, palette):
                 self.register_palette_entry(*item)
                 continue
             if len(item) != 2:
-                raise ScreenError("Invalid register_palette entry: %s" %
-                    repr(item))
+                raise ScreenError(f"Invalid register_palette entry: {item!r}")
             name, like_name = item
             if like_name not in self._palette:
-                raise ScreenError("palette entry '%s' doesn't exist"%like_name)
+                raise ScreenError(f"palette entry '{like_name}' doesn't exist")
             self._palette[name] = self._palette[like_name]
 
     def register_palette_entry(self, name, foreground, background,
diff --git a/urwid/escape.py b/urwid/escape.py
index 6992834be..ec8da4e66 100644
--- a/urwid/escape.py
+++ b/urwid/escape.py
@@ -1,5 +1,4 @@
 #!/usr/bin/python
-# -*- coding: utf-8 -*-
 #
 # Urwid escape sequences common to curses_display and raw_display
 #    Copyright (C) 2004-2011  Ian Ward
@@ -18,14 +17,15 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
 """
 Terminal Escape Sequences for input and display
 """
 
+from __future__ import annotations
+
 import re
 
 try:
@@ -33,8 +33,6 @@
 except ImportError:
     from urwid import old_str_util as str_util
 
-from urwid.compat import bytes3
-
 # NOTE: because of circular imports (urwid.util -> urwid.escape -> urwid.util)
 # from urwid.util import is_mouse_event -- will not work here
 import urwid.util
@@ -47,16 +45,16 @@
 IBMPC_OFF = "\x1b[10m"
 
 DEC_TAG = "0"
-DEC_SPECIAL_CHARS = u'▮◆▒␉␌␍␊°±␤␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π≠£·'
-ALT_DEC_SPECIAL_CHARS = u"_`abcdefghijklmnopqrstuvwxyz{|}~"
+DEC_SPECIAL_CHARS = '▮◆▒␉␌␍␊°±␤␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π≠£·'
+ALT_DEC_SPECIAL_CHARS = "_`abcdefghijklmnopqrstuvwxyz{|}~"
 
 DEC_SPECIAL_CHARMAP = {}
 assert len(DEC_SPECIAL_CHARS) == len(ALT_DEC_SPECIAL_CHARS), repr((DEC_SPECIAL_CHARS, ALT_DEC_SPECIAL_CHARS))
 for c, alt in zip(DEC_SPECIAL_CHARS, ALT_DEC_SPECIAL_CHARS):
     DEC_SPECIAL_CHARMAP[ord(c)] = SO + alt + SI
 
-SAFE_ASCII_DEC_SPECIAL_RE = re.compile(u"^[ -~%s]*$" % DEC_SPECIAL_CHARS)
-DEC_SPECIAL_RE = re.compile(u"[%s]" % DEC_SPECIAL_CHARS)
+SAFE_ASCII_DEC_SPECIAL_RE = re.compile(f"^[ -~{DEC_SPECIAL_CHARS}]*$")
+DEC_SPECIAL_RE = re.compile(f"[{DEC_SPECIAL_CHARS}]")
 
 
 ###################
@@ -101,12 +99,12 @@ def escape_modifier( digit ):
     for prefix, modifier in zip('O[', ('meta ', 'shift '))
     for letter, key in zip('abcd', ('up', 'down', 'right', 'left'))
 ] + [
-    ("[" + digit + symbol, modifier + key)
+    (f"[{digit}{symbol}", modifier + key)
     for modifier, symbol in zip(('shift ', 'meta '), '$^')
     for digit, key in zip('235678',
         ('insert', 'delete', 'page up', 'page down', 'home', 'end'))
 ] + [
-    ('O' + chr(ord('p')+n), str(n)) for n in range(10)
+    (f"O{chr(ord('p') + n)}", str(n)) for n in range(10)
 ] + [
     # modified cursor keys + home, end, 5 -- [#X and [1;#X forms
     (prefix+digit+letter, escape_modifier(digit) + key)
@@ -116,12 +114,12 @@ def escape_modifier( digit ):
         ('up','down','right','left','5','end','5','home'))
 ] + [
     # modified F1-F4 keys -- O#X form
-    ("O"+digit+letter, escape_modifier(digit) + key)
+    (f"O{digit}{letter}", escape_modifier(digit) + key)
     for digit in "12345678"
     for letter,key in zip("PQRS",('f1','f2','f3','f4'))
 ] + [
     # modified F1-F13 keys -- [XX;#~ form
-    ("["+str(num)+";"+digit+"~", escape_modifier(digit) + key)
+    (f"[{str(num)};{digit}~", escape_modifier(digit) + key)
     for digit in "12345678"
     for num,key in zip(
         (3,5,6,11,12,13,14,15,17,18,19,20,21,23,24,25,26,28,29,31,32,33,34),
@@ -139,7 +137,7 @@ def escape_modifier( digit ):
     ('[0n', 'status ok')
 ]
 
-class KeyqueueTrie(object):
+class KeyqueueTrie:
     def __init__( self, sequences ):
         self.data = {}
         for s, result in sequences:
@@ -191,11 +189,11 @@ def read_mouse_info(self, keys, more_available):
         x, y = (keys[1] - 33)%256, (keys[2] - 33)%256  # supports 0-255
 
         prefix = ""
-        if b & 4:    prefix = prefix + "shift "
-        if b & 8:    prefix = prefix + "meta "
-        if b & 16:    prefix = prefix + "ctrl "
-        if (b & MOUSE_MULTIPLE_CLICK_MASK)>>9 == 1:    prefix = prefix + "double "
-        if (b & MOUSE_MULTIPLE_CLICK_MASK)>>9 == 2:    prefix = prefix + "triple "
+        if b & 4:    prefix = f"{prefix}shift "
+        if b & 8:    prefix = f"{prefix}meta "
+        if b & 16:    prefix = f"{prefix}ctrl "
+        if (b & MOUSE_MULTIPLE_CLICK_MASK)>>9 == 1:    prefix = f"{prefix}double "
+        if (b & MOUSE_MULTIPLE_CLICK_MASK)>>9 == 2:    prefix = f"{prefix}triple "
 
         # 0->1, 1->2, 2->3, 64->4, 65->5
         button = ((b&64)//64*3) + (b & 3) + 1
@@ -212,7 +210,7 @@ def read_mouse_info(self, keys, more_available):
         else:
             action = "press"
 
-        return ( (prefix + "mouse " + action, button, x, y), keys[3:] )
+        return ( (f"{prefix}mouse {action}", button, x, y), keys[3:] )
 
     def read_sgrmouse_info(self, keys, more_available):
         # Helpful links:
@@ -228,10 +226,10 @@ def read_sgrmouse_info(self, keys, more_available):
         pos_m = 0
         found_m = False
         for k in keys:
-            value = value + chr(k);
+            value = value + chr(k)
             if ((k is ord('M')) or (k is ord('m'))):
                 found_m = True
-                break;
+                break
             pos_m += 1
         if not found_m:
             if more_available:
@@ -261,7 +259,7 @@ def read_sgrmouse_info(self, keys, more_available):
         else:
             action = "release"
 
-        return ( ("mouse " + action, button, x, y), keys[pos_m + 1:] )
+        return ( (f"mouse {action}", button, x, y), keys[pos_m + 1:] )
 
 
     def read_cursor_position(self, keys, more_available):
@@ -385,9 +383,9 @@ def process_keyqueue(codes, more_available):
     if code in _keyconv:
         return [_keyconv[code]], codes[1:]
     if code >0 and code <27:
-        return ["ctrl %s" % chr(ord('a')+code-1)], codes[1:]
+        return [f"ctrl {chr(ord('a') + code - 1)}"], codes[1:]
     if code >27 and code <32:
-        return ["ctrl %s" % chr(ord('A')+code-1)], codes[1:]
+        return [f"ctrl {chr(ord('A') + code - 1)}"], codes[1:]
 
     em = str_util.get_byte_encoding()
 
@@ -420,7 +418,7 @@ def process_keyqueue(codes, more_available):
             if k>256 or k&0xc0 != 0x80:
                 return ["<%d>"%code], codes[1:]
 
-        s = bytes3(codes[:need_more+1])
+        s = bytes(codes[:need_more+1])
 
         assert isinstance(s, bytes)
         try:
@@ -448,7 +446,7 @@ def process_keyqueue(codes, more_available):
             return ['esc'] + run, remaining_codes
         if run[0] == "esc" or run[0].find("meta ") >= 0:
             return ['esc']+run, remaining_codes
-        return ['meta '+run[0]]+run[1:], remaining_codes
+        return [f"meta {run[0]}"]+run[1:], remaining_codes
 
     return ['esc'], codes[1:]
 
@@ -459,23 +457,23 @@ def process_keyqueue(codes, more_available):
 
 ESC = "\x1b"
 
-CURSOR_HOME = ESC+"[H"
+CURSOR_HOME = f"{ESC}[H"
 CURSOR_HOME_COL = "\r"
 
-APP_KEYPAD_MODE = ESC+"="
-NUM_KEYPAD_MODE = ESC+">"
+APP_KEYPAD_MODE = f"{ESC}="
+NUM_KEYPAD_MODE = f"{ESC}>"
 
-SWITCH_TO_ALTERNATE_BUFFER = ESC+"7"+ESC+"[?47h"
-RESTORE_NORMAL_BUFFER = ESC+"[?47l"+ESC+"8"
+SWITCH_TO_ALTERNATE_BUFFER = f"{ESC}7{ESC}[?47h"
+RESTORE_NORMAL_BUFFER = f"{ESC}[?47l{ESC}8"
 
 #RESET_SCROLL_REGION = ESC+"[;r"
 #RESET = ESC+"c"
 
-REPORT_STATUS = ESC + "[5n"
-REPORT_CURSOR_POSITION = ESC+"[6n"
+REPORT_STATUS = f"{ESC}[5n"
+REPORT_CURSOR_POSITION = f"{ESC}[6n"
 
-INSERT_ON = ESC + "[4h"
-INSERT_OFF = ESC + "[4l"
+INSERT_ON = f"{ESC}[4h"
+INSERT_OFF = f"{ESC}[4l"
 
 def set_cursor_position( x, y ):
     assert type(x) == int
@@ -494,12 +492,12 @@ def move_cursor_down(x):
     if x < 1: return ""
     return ESC+"[%dB" % x
 
-HIDE_CURSOR = ESC+"[?25l"
-SHOW_CURSOR = ESC+"[?25h"
+HIDE_CURSOR = f"{ESC}[?25l"
+SHOW_CURSOR = f"{ESC}[?25h"
 
-MOUSE_TRACKING_ON = ESC+"[?1000h"+ESC+"[?1002h"+ESC+"[?1006h"
-MOUSE_TRACKING_OFF = ESC+"[?1006l"+ESC+"[?1002l"+ESC+"[?1000l"
+MOUSE_TRACKING_ON = f"{ESC}[?1000h{ESC}[?1002h{ESC}[?1006h"
+MOUSE_TRACKING_OFF = f"{ESC}[?1006l{ESC}[?1002l{ESC}[?1000l"
 
-DESIGNATE_G1_SPECIAL = ESC+")0"
+DESIGNATE_G1_SPECIAL = f"{ESC})0"
 
-ERASE_IN_LINE_RIGHT = ESC+"[K"
+ERASE_IN_LINE_RIGHT = f"{ESC}[K"
diff --git a/urwid/font.py b/urwid/font.py
index 30749a652..42dc647d1 100755
--- a/urwid/font.py
+++ b/urwid/font.py
@@ -1,5 +1,4 @@
 #!/usr/bin/python
-# -*- coding: utf-8 -*-
 #
 # Urwid BigText fonts
 #    Copyright (C) 2004-2006  Ian Ward
@@ -18,14 +17,14 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
+from __future__ import annotations
+
+from urwid.canvas import TextCanvas
 from urwid.escape import SAFE_ASCII_DEC_SPECIAL_RE
 from urwid.util import apply_target_encoding, str_util
-from urwid.canvas import TextCanvas
-from urwid.compat import text_type
 
 
 def separate_glyphs(gdata, height):
@@ -92,7 +91,7 @@ def add_font(name, cls):
     _all_fonts.append((name, cls))
 
 
-class Font(object):
+class Font:
     def __init__(self):
         assert self.height
         assert self.data
@@ -105,7 +104,7 @@ def __init__(self):
 
     @staticmethod
     def _to_text(obj, encoding='utf-8', errors='strict'):
-        if isinstance(obj, text_type):
+        if isinstance(obj, str):
             return obj
         elif isinstance(obj, bytes):
             return obj.decode(encoding, errors)
@@ -116,8 +115,8 @@ def add_glyphs(self, gdata):
         self.utf8_required |= utf8_required
 
     def characters(self):
-        l = list(self.char.keys())
-        l.sort()
+        l = sorted(self.char)
+
         return "".join(l)
 
     def char_width(self, c):
@@ -152,7 +151,7 @@ def render(self, c):
 
 class Thin3x3Font(Font):
     height = 3
-    data = [u"""
+    data = ["""
 000111222333444555666777888999  !
 ┌─┐ ┐ ┌─┐┌─┐  ┐┌─ ┌─ ┌─┐┌─┐┌─┐  │
 │ │ │ ┌─┘ ─┤└─┼└─┐├─┐  ┼├─┤└─┤  │
@@ -167,7 +166,7 @@ class Thin3x3Font(Font):
 
 class Thin4x3Font(Font):
     height = 3
-    data = Thin3x3Font.data + [u"""
+    data = Thin3x3Font.data + ["""
 0000111122223333444455556666777788889999  ####$$$$
 ┌──┐  ┐ ┌──┐┌──┐   ┐┌── ┌── ┌──┐┌──┐┌──┐   ┼─┼┌┼┼┐
 │  │  │ ┌──┘  ─┤└──┼└──┐├──┐   ┼├──┤└──┤   ┼─┼└┼┼┐
@@ -177,13 +176,13 @@ class Thin4x3Font(Font):
 
 class HalfBlock5x4Font(Font):
     height = 4
-    data = [u"""
+    data = ["""
 00000111112222233333444445555566666777778888899999  !!
 ▄▀▀▄  ▄█  ▄▀▀▄ ▄▀▀▄ ▄  █ █▀▀▀ ▄▀▀  ▀▀▀█ ▄▀▀▄ ▄▀▀▄   █
 █  █   █    ▄▀   ▄▀ █▄▄█ █▄▄  █▄▄    ▐▌ ▀▄▄▀ ▀▄▄█   █
 █  █   █  ▄▀   ▄  █    █    █ █  █   █  █  █    █   ▀
  ▀▀   ▀▀▀ ▀▀▀▀  ▀▀     ▀ ▀▀▀   ▀▀    ▀   ▀▀   ▀▀    ▀
-""", u'''
+""", '''
 """######$$$$$$%%%%%&&&&&((()))******++++++,,,-----..////::;;;
 █▐▌ █ █  ▄▀█▀▄ ▐▌▐▌ ▄▀▄   █ █   ▄ ▄    ▄              ▐▌
    ▀█▀█▀ ▀▄█▄    █  ▀▄▀  ▐▌ ▐▌ ▄▄█▄▄ ▄▄█▄▄    ▄▄▄▄    █  ▀  ▀
@@ -195,37 +194,37 @@ class HalfBlock5x4Font(Font):
 ▄▀   ▀▀▀▀   ▀▄   ▄▀ █ █▀█ █    █    █            ▄▀  █  ▀▄ ▐▐▌▌
  ▀▄  ▀▀▀▀  ▄▀    ▀  █ ▀▀▀ █    ▐▌   █             █  █  █    ▀
    ▀      ▀      ▀   ▀▀▀  ▀▀▀   ▀ ▀▀▀     ▀▀▀▀     ▀ ▀ ▀
-""", u'''
+""", '''
 AAAAABBBBBCCCCCDDDDDEEEEEFFFFFGGGGGHHHHHIIJJJJJKKKKK
 ▄▀▀▄ █▀▀▄ ▄▀▀▄ █▀▀▄ █▀▀▀ █▀▀▀ ▄▀▀▄ █  █ █    █ █  █
 █▄▄█ █▄▄▀ █    █  █ █▄▄  █▄▄  █    █▄▄█ █    █ █▄▀
 █  █ █  █ █  ▄ █  █ █    █    █ ▀█ █  █ █ ▄  █ █ ▀▄
 ▀  ▀ ▀▀▀   ▀▀  ▀▀▀  ▀▀▀▀ ▀     ▀▀  ▀  ▀ ▀  ▀▀  ▀  ▀
-''', u'''
+''', '''
 LLLLLMMMMMMNNNNNOOOOOPPPPPQQQQQRRRRRSSSSSTTTTT
 █    █▄ ▄█ ██ █ ▄▀▀▄ █▀▀▄ ▄▀▀▄ █▀▀▄ ▄▀▀▄ ▀▀█▀▀
 █    █ ▀ █ █▐▌█ █  █ █▄▄▀ █  █ █▄▄▀ ▀▄▄    █
 █    █   █ █ ██ █  █ █    █ ▌█ █  █ ▄  █   █
 ▀▀▀▀ ▀   ▀ ▀  ▀  ▀▀  ▀     ▀▀▌ ▀  ▀  ▀▀    ▀
-''', u'''
+''', '''
 UUUUUVVVVVVWWWWWWXXXXXXYYYYYYZZZZZ
 █  █ █   █ █   █ █   █ █   █ ▀▀▀█
 █  █ ▐▌ ▐▌ █ ▄ █  ▀▄▀   ▀▄▀   ▄▀
 █  █  █ █  ▐▌█▐▌ ▄▀ ▀▄   █   █
  ▀▀    ▀    ▀ ▀  ▀   ▀   ▀   ▀▀▀▀
-''', u'''
+''', '''
 aaaaabbbbbcccccdddddeeeeeffffggggghhhhhiijjjjkkkkk
      █            █       ▄▀▀     █    ▄   ▄ █
  ▀▀▄ █▀▀▄ ▄▀▀▄ ▄▀▀█ ▄▀▀▄ ▀█▀ ▄▀▀▄ █▀▀▄ ▄   ▄ █ ▄▀
 ▄▀▀█ █  █ █  ▄ █  █ █▀▀   █  ▀▄▄█ █  █ █   █ █▀▄
  ▀▀▀ ▀▀▀   ▀▀   ▀▀▀  ▀▀   ▀   ▄▄▀ ▀  ▀ ▀ ▄▄▀ ▀  ▀
-''', u'''
+''', '''
 llmmmmmmnnnnnooooopppppqqqqqrrrrssssstttt
 █                                     █
 █ █▀▄▀▄ █▀▀▄ ▄▀▀▄ █▀▀▄ ▄▀▀█ █▀▀ ▄▀▀▀ ▀█▀
 █ █ █ █ █  █ █  █ █  █ █  █ █    ▀▀▄  █
 ▀ ▀   ▀ ▀  ▀  ▀▀  █▀▀   ▀▀█ ▀   ▀▀▀    ▀
-''', u'''
+''', '''
 uuuuuvvvvvwwwwwwxxxxxxyyyyyzzzzz
 
 █  █ █  █ █ ▄ █ ▀▄ ▄▀ █  █ ▀▀█▀
@@ -236,7 +235,7 @@ class HalfBlock5x4Font(Font):
 
 class HalfBlock6x5Font(Font):
     height = 5
-    data = [u"""
+    data = ["""
 000000111111222222333333444444555555666666777777888888999999  ..::////
 ▄▀▀▀▄  ▄█   ▄▀▀▀▄ ▄▀▀▀▄ ▄  █  █▀▀▀▀ ▄▀▀▀  ▀▀▀▀█ ▄▀▀▀▄ ▄▀▀▀▄         █
 █   █   █       █     █ █  █  █     █        ▐▌ █   █ █   █     ▀  ▐▌
@@ -248,7 +247,7 @@ class HalfBlock6x5Font(Font):
 
 class HalfBlockHeavy6x5Font(Font):
     height = 5
-    data = [u"""
+    data = ["""
 000000111111222222333333444444555555666666777777888888999999  ..::////
 ▄███▄  ▐█▌  ▄███▄ ▄███▄    █▌ █████ ▄███▄ █████ ▄███▄ ▄███▄         █▌
 █▌ ▐█  ▀█▌  ▀  ▐█ ▀  ▐█ █▌ █▌ █▌    █▌       █▌ █▌ ▐█ █▌ ▐█     █▌ ▐█
@@ -260,7 +259,7 @@ class HalfBlockHeavy6x5Font(Font):
 
 class Thin6x6Font(Font):
     height = 6
-    data = [u"""
+    data = ["""
 000000111111222222333333444444555555666666777777888888999999''
 ┌───┐   ┐   ┌───┐ ┌───┐    ┐  ┌───  ┌───  ┌───┐ ┌───┐ ┌───┐ │
 │   │   │       │     │ ┌  │  │     │         │ │   │ │   │
@@ -292,7 +291,7 @@ class Thin6x6Font(Font):
 │    \  │          │ │ │
 └     \ ┘    ────  └ │ ┘
 
-""", u"""
+""", """
 AAAAAABBBBBBCCCCCCDDDDDDEEEEEEFFFFFFGGGGGGHHHHHHIIJJJJJJ
 ┌───┐ ┬───┐ ┌───┐ ┬───┐ ┬───┐ ┬───┐ ┌───┐ ┬   ┬ ┬     ┬
 │   │ │   │ │     │   │ │     │     │     │   │ │     │
@@ -300,7 +299,7 @@ class Thin6x6Font(Font):
 │   │ │   │ │     │   │ │     │     │   │ │   │ │ ┬   │
 ┴   ┴ ┴───┘ └───┘ ┴───┘ ┴───┘ ┴     └───┘ ┴   ┴ ┴ └───┘
 
-""", u"""
+""", """
 KKKKKKLLLLLLMMMMMMNNNNNNOOOOOOPPPPPPQQQQQQRRRRRRSSSSSS
 ┬   ┬ ┬     ┌─┬─┐ ┬─┐ ┬ ┌───┐ ┬───┐ ┌───┐ ┬───┐ ┌───┐
 │ ┌─┘ │     │ │ │ │ │ │ │   │ │   │ │   │ │   │ │
@@ -308,7 +307,7 @@ class Thin6x6Font(Font):
 │  └┐ │     │   │ │ │ │ │   │ │     │  ┐│ │ └─┐     │
 ┴   ┴ ┴───┘ ┴   ┴ ┴ └─┴ └───┘ ┴     └──┼┘ ┴   ┴ └───┘
                                        └
-""", u"""
+""", """
 TTTTTTUUUUUUVVVVVVWWWWWWXXXXXXYYYYYYZZZZZZ
 ┌─┬─┐ ┬   ┬ ┬   ┬ ┬   ┬ ┬   ┬ ┬   ┬ ┌───┐
   │   │   │ │   │ │   │ └┐ ┌┘ │   │   ┌─┘
@@ -316,7 +315,7 @@ class Thin6x6Font(Font):
   │   │   │ └┐ ┌┘ │ │ │ ┌┘ └┐   │   ┌┘
   ┴   └───┘  └─┘  └─┴─┘ ┴   ┴   ┴   └───┘
 
-""", u"""
+""", """
 aaaaaabbbbbbccccccddddddeeeeeefffgggggghhhhhhiijjj
                               ┌─┐
       │               │       │        │     .  .
@@ -324,7 +323,7 @@ class Thin6x6Font(Font):
 ┌───┤ │   │ │     │   │ ├───┘ │  │   │ │   │ │  │
 └───┴ └───┘ └───┘ └───┘ └───┘ ┴  └───┤ ┴   ┴ ┴  │
                                  └───┘         ─┘
-""", u"""
+""", """
 kkkkkkllmmmmmmnnnnnnooooooppppppqqqqqqrrrrrssssss
 
 │     │
@@ -332,7 +331,7 @@ class Thin6x6Font(Font):
 ├─┴┐  │ │ │ │ │   │ │   │ │   │ │   │ │    └───┐
 ┴  └─ └ ┴   ┴ ┴   ┴ └───┘ ├───┘ └───┤ ┴    └───┘
                           │         │
-""", u"""
+""", """
 ttttuuuuuuvvvvvvwwwwwwxxxxxxyyyyyyzzzzzz
 
  │
@@ -346,7 +345,7 @@ class Thin6x6Font(Font):
 
 class HalfBlock7x7Font(Font):
     height = 7
-    data = [u"""
+    data = ["""
 0000000111111122222223333333444444455555556666666777777788888889999999'''
  ▄███▄   ▐█▌   ▄███▄  ▄███▄     █▌ ▐█████▌ ▄███▄ ▐█████▌ ▄███▄  ▄███▄ ▐█
 ▐█   █▌  ▀█▌  ▐█   █▌▐█   █▌▐█  █▌ ▐█     ▐█         ▐█ ▐█   █▌▐█   █▌▐█
@@ -355,7 +354,7 @@ class HalfBlock7x7Font(Font):
 ▐█   █▌   █▌   ▄█▀   ▐█   █▌    █▌      █▌▐█   █▌   █▌  ▐█   █▌     █▌
  ▀███▀   ███▌ ▐█████▌ ▀███▀     █▌ ▐████▀  ▀███▀   ▐█    ▀███▀  ▀███▀
 
-""", u'''
+""", '''
 !!!   """""#######$$$$$$$%%%%%%%&&&&&&&(((())))*******++++++
 ▐█    ▐█ █▌ ▐█ █▌    █    ▄  █▌   ▄█▄    █▌▐█   ▄▄ ▄▄
 ▐█    ▐█ █▌▐█████▌ ▄███▄ ▐█▌▐█   ▐█ █▌  ▐█  █▌  ▀█▄█▀   ▐█
@@ -364,7 +363,7 @@ class HalfBlock7x7Font(Font):
             ▐█ █▌  ▀███▀   █▌▐█▌▐█  █▌  ▐█  █▌  ▀▀ ▀▀
 ▐█                   █    ▐█  ▀  ▀██▀█▌  █▌▐█
 
-''', u"""
+''', """
 ,,,------.../////:::;;;<<<<<<<======>>>>>>>???????@@@@@@@
                █▌          ▄█▌      ▐█▄     ▄███▄  ▄███▄
               ▐█ ▐█ ▐█   ▄█▀  ▐████▌  ▀█▄  ▐█   █▌▐█ ▄▄█▌
@@ -382,7 +381,7 @@ class HalfBlock7x7Font(Font):
 ▐█    ▐█   █▌                 █▌ ▐█  ▐█
 ▐██▌   █▌▐██▌       █████      █▌▐█ ▐█
 
-""", u"""
+""", """
 AAAAAAABBBBBBBCCCCCCCDDDDDDDEEEEEEEFFFFFFFGGGGGGGHHHHHHHIIIIJJJJJJJ
  ▄███▄ ▐████▄  ▄███▄ ▐████▄ ▐█████▌▐█████▌ ▄███▄ ▐█   █▌ ██▌     █▌
 ▐█   █▌▐█   █▌▐█     ▐█   █▌▐█     ▐█     ▐█     ▐█   █▌ ▐█      █▌
@@ -391,7 +390,7 @@ class HalfBlock7x7Font(Font):
 ▐█   █▌▐█   █▌▐█     ▐█   █▌▐█     ▐█     ▐█   █▌▐█   █▌ ▐█ ▐█   █▌
 ▐█   █▌▐████▀  ▀███▀ ▐████▀ ▐█████▌▐█      ▀███▀ ▐█   █▌ ██▌ ▀███▀
 
-""", u"""
+""", """
 KKKKKKKLLLLLLLMMMMMMMMNNNNNNNOOOOOOOPPPPPPPQQQQQQQRRRRRRRSSSSSSS
 ▐█   █▌▐█      ▄█▌▐█▄ ▐██  █▌ ▄███▄ ▐████▄  ▄███▄ ▐████▄  ▄███▄
 ▐█  █▌ ▐█     ▐█ ▐▌ █▌▐██▌ █▌▐█   █▌▐█   █▌▐█   █▌▐█   █▌▐█
@@ -400,7 +399,7 @@ class HalfBlock7x7Font(Font):
 ▐█  █▌ ▐█     ▐█    █▌▐█ ▐██▌▐█   █▌▐█     ▐█ █▌█▌▐█   █▌     █▌
 ▐█   █▌▐█████▌▐█    █▌▐█  ██▌ ▀███▀ ▐█      ▀███▀ ▐█   █▌ ▀███▀
                                                ▀▀
-""", u"""
+""", """
 TTTTTTTUUUUUUUVVVVVVVWWWWWWWWXXXXXXXYYYYYYYZZZZZZZ
  █████▌▐█   █▌▐█   █▌▐█    █▌▐█   █▌ █▌  █▌▐█████▌
    █▌  ▐█   █▌ █▌ ▐█ ▐█    █▌ ▐█ █▌  ▐█ ▐█     █▌
@@ -409,7 +408,7 @@ class HalfBlock7x7Font(Font):
    █▌  ▐█   █▌  ▐█▌  ▐█ ▐▌ █▌ █▌ ▐█    █▌   █▌
    █▌   ▀███▀    █    ▀█▌▐█▀ ▐█   █▌   █▌  ▐█████▌
 
-""", u"""
+""", """
 aaaaaaabbbbbbbcccccccdddddddeeeeeeefffffggggggghhhhhhhiiijjjj
        ▐█                 █▌         ▄█▌       ▐█      █▌  █▌
        ▐█                 █▌        ▐█         ▐█
@@ -418,7 +417,7 @@ class HalfBlock7x7Font(Font):
 ▐█▀▀▀█▌▐█   █▌▐█     ▐█   █▌▐█▀▀▀   ▐█  ▐█▄▄▄█▌▐█   █▌ █▌  █▌
  ▀████▌▐████▀  ▀███▀  ▀████▌ ▀███▀  ▐█    ▀▀▀█▌▐█   █▌ █▌  █▌
                                          ▀███▀           ▐██
-""", u"""
+""", """
 kkkkkkkllllmmmmmmmmnnnnnnnooooooopppppppqqqqqqqrrrrrrsssssss
 ▐█      ██
 ▐█      ▐█
@@ -427,7 +426,7 @@ class HalfBlock7x7Font(Font):
 ▐█▀▀█▄  ▐█ ▐█ ▐▌ █▌▐█   █▌▐█   █▌▐█   █▌▐█   █▌▐█      ▀▀▀█▌
 ▐█   █▌ ▐█▌▐█    █▌▐█   █▌ ▀███▀ ▐████▀  ▀████▌▐█     ▀███▀
                                  ▐█          █▌
-""", u"""
+""", """
 tttttuuuuuuuvvvvvvvwwwwwwwwxxxxxxxyyyyyyyzzzzzzz
   █▌
   █▌
@@ -455,6 +454,6 @@ class HalfBlock7x7Font(Font):
         if c == all_ascii:
             print("Full ASCII")
         elif c.startswith(all_ascii):
-            print("Full ASCII + " + c[len(all_ascii):])
+            print(f"Full ASCII + {c[len(all_ascii):]}")
         else:
-            print("Characters: " + c)
+            print(f"Characters: {c}")
diff --git a/urwid/graphics.py b/urwid/graphics.py
index f94b2a9f1..71c079180 100755
--- a/urwid/graphics.py
+++ b/urwid/graphics.py
@@ -1,5 +1,4 @@
 #!/usr/bin/python
-# -*- coding: utf-8 -*-
 #
 # Urwid graphics widgets
 #    Copyright (C) 2004-2011  Ian Ward
@@ -18,20 +17,38 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
-from urwid.compat import ord2, with_metaclass
-from urwid.util import decompose_tagmarkup, get_encoding_mode
-from urwid.canvas import CompositeCanvas, CanvasJoin, TextCanvas, \
-    CanvasCombine, SolidCanvas
-from urwid.widget import WidgetMeta, Widget, BOX, FIXED, FLOW, \
-    nocache_widget_render, nocache_widget_render_instance, fixed_size, \
-    WidgetWrap, Divider, SolidFill, Text, CENTER, CLIP
-from urwid.container import Pile, Columns
-from urwid.display_common import AttrSpec
+from __future__ import annotations
+
+from urwid.canvas import (
+    CanvasCombine,
+    CanvasJoin,
+    CompositeCanvas,
+    SolidCanvas,
+    TextCanvas,
+)
+from urwid.container import Columns, Pile
 from urwid.decoration import WidgetDecoration
+from urwid.display_common import AttrSpec
+from urwid.util import decompose_tagmarkup, get_encoding_mode
+from urwid.widget import (
+    BOX,
+    CENTER,
+    CLIP,
+    FIXED,
+    FLOW,
+    Divider,
+    SolidFill,
+    Text,
+    Widget,
+    WidgetMeta,
+    WidgetWrap,
+    fixed_size,
+    nocache_widget_render,
+    nocache_widget_render_instance,
+)
 
 
 class BigText(Widget):
@@ -101,9 +118,9 @@ class LineBox(WidgetDecoration, WidgetWrap):
 
     def __init__(self, original_widget, title="",
                  title_align="center", title_attr=None,
-                 tlcorner=u'┌', tline=u'─', lline=u'│',
-                 trcorner=u'┐', blcorner=u'└', rline=u'│',
-                 bline=u'─', brcorner=u'┘'):
+                 tlcorner='┌', tline='─', lline='│',
+                 trcorner='┐', blcorner='└', rline='│',
+                 bline='─', brcorner='┘'):
         """
         Draw a line around original_widget.
 
@@ -175,7 +192,7 @@ def __init__(self, original_widget, title="",
         else:
             # Note: We need to define a fixed first widget (even if it's 0 width) so that the other
             # widgets have something to anchor onto
-            middle_widgets.append(('fixed', 0, SolidFill(u"")))
+            middle_widgets.append(('fixed', 0, SolidFill("")))
         middle_widgets.append(original_widget)
         focus_col = len(middle_widgets) - 1
         if rline:
@@ -205,7 +222,7 @@ def __init__(self, original_widget, title="",
 
     def format_title(self, text):
         if len(text) > 0:
-            return " %s " % text
+            return f" {text} "
         else:
             return ""
 
@@ -240,7 +257,7 @@ class BarGraphMeta(WidgetMeta):
     should use set_data() instead of overriding get_data().
     """
     def __init__(cls, name, bases, d):
-        super(BarGraphMeta, cls).__init__(name, bases, d)
+        super().__init__(name, bases, d)
 
         if "get_data" in d:
             cls.render = nocache_widget_render(cls)
@@ -261,13 +278,13 @@ def nocache_bargraph_get_data(self, get_data_fn):
 class BarGraphError(Exception):
     pass
 
-class BarGraph(with_metaclass(BarGraphMeta, Widget)):
+class BarGraph(Widget, metaclass=BarGraphMeta):
     _sizing = frozenset([BOX])
 
     ignore_focus = True
 
-    eighths = u' ▁▂▃▄▅▆▇'
-    hlines = u'_⎺⎻─⎼⎽'
+    eighths = ' ▁▂▃▄▅▆▇'
+    hlines = '_⎺⎻─⎼⎽'
 
     def __init__(self, attlist, hatt=None, satt=None):
         """
@@ -317,7 +334,7 @@ def set_segment_attributes(self, attlist, hatt=None, satt=None):
         self.attr = []
         self.char = []
         if len(attlist) < 2:
-            raise BarGraphError("attlist must include at least background and seg1: %r" % (attlist,))
+            raise BarGraphError(f"attlist must include at least background and seg1: {attlist!r}")
         assert len(attlist) >= 2, 'must at least specify bg and fg!'
         for a in attlist:
             if type(a) != tuple:
@@ -341,13 +358,13 @@ def set_segment_attributes(self, attlist, hatt=None, satt=None):
             try:
                 (fg, bg), attr = i
             except ValueError:
-                raise BarGraphError("satt not in (fg,bg:attr) form: %r" % (i,))
+                raise BarGraphError(f"satt not in (fg,bg:attr) form: {i!r}")
             if type(fg) != int or fg >= len(attlist):
-                raise BarGraphError("fg not valid integer: %r" % (fg,))
+                raise BarGraphError(f"fg not valid integer: {fg!r}")
             if type(bg) != int or bg >= len(attlist):
-                raise BarGraphError("bg not valid integer: %r" % (fg,))
+                raise BarGraphError(f"bg not valid integer: {fg!r}")
             if fg <= bg:
-                raise BarGraphError("fg (%s) not > bg (%s)" % (fg, bg))
+                raise BarGraphError(f"fg ({fg}) not > bg ({bg})")
         self.satt = satt
 
     def set_data(self, bardata, top, hlines=None):
@@ -369,9 +386,8 @@ def set_data(self, bardata, top, hlines=None):
         and it will have a second segment that starts at 30%.
         """
         if hlines is not None:
-            hlines = hlines[:]  # shallow copy
-            hlines.sort()
-            hlines.reverse()
+            hlines = sorted(hlines[:], reverse=True)  # shallow copy
+
         self.data = bardata, top, hlines
         self._invalidate()
 
@@ -713,7 +729,7 @@ def add_segment(seg_num, col, row, width, rows=rows):
         col += width
         barnum += 1
 
-    #print repr(rows)
+    # print(repr(rows))
     # build rowsets data structure
     rowsets = []
     y_count = 0
@@ -807,9 +823,8 @@ def set_scale(self, labels, top):
         top -- top y position
         """
 
-        labels = labels[:]  # shallow copy
-        labels.sort()
-        labels.reverse()
+        labels = sorted(labels[:], reverse=True)  # shallow copy
+
         self.pos = []
         self.txt = []
         for y, markup in labels:
@@ -865,7 +880,7 @@ def scale_bar_values( bar, top, maxrow ):
 class ProgressBar(Widget):
     _sizing = frozenset([FLOW])
 
-    eighths = u' ▏▎▍▌▋▊▉'
+    eighths = ' ▏▎▍▌▋▊▉'
 
     text_align = CENTER
 
@@ -933,7 +948,7 @@ def get_text(self):
         You can override this method to display custom text.
         """
         percent = min(100, max(0, int(self.current * 100 / self.done)))
-        return str(percent) + " %"
+        return f"{str(percent)} %"
 
     def render(self, size, focus=False):
         """
@@ -957,7 +972,7 @@ def render(self, size, focus=False):
             c._attr = [[(self.normal, maxcol)]]
         elif ccol >= maxcol:
             c._attr = [[(self.complete, maxcol)]]
-        elif cs and ord2(c._text[0][ccol]) == 32:
+        elif cs and c._text[0][ccol] == 32:
             t = c._text[0]
             cenc = self.eighths[cs].encode("utf-8")
             c._text[0] = t[:ccol] + cenc + t[ccol + 1:]
diff --git a/urwid/html_fragment.py b/urwid/html_fragment.py
index 5df927371..b14c57f96 100755
--- a/urwid/html_fragment.py
+++ b/urwid/html_fragment.py
@@ -17,18 +17,18 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
 """
 HTML PRE-based UI implementation
 """
 
+from __future__ import annotations
+
 from urwid import util
-from urwid.main_loop import ExitMainLoop
 from urwid.display_common import AttrSpec, BaseScreen
-
+from urwid.main_loop import ExitMainLoop
 
 # replace control characters with ?'s
 _trans_table = "?" * 32 + "".join([chr(x) for x in range(32, 256)])
@@ -47,7 +47,7 @@ class HtmlGenerator(BaseScreen):
     started = True
 
     def __init__(self):
-        super(HtmlGenerator, self).__init__()
+        super().__init__()
         self.colors = 16
         self.bright_is_bold = False # ignored
         self.has_underline = True # ignored
@@ -124,7 +124,7 @@ def draw_screen(self, size, r ):
             l.append("\n")
 
         # add the fragment to the list
-        self.fragments.append( "<pre>%s</pre>" % "".join(l) )
+        self.fragments.append( f"<pre>{''.join(l)}</pre>" )
 
     def clear(self):
         """
@@ -160,8 +160,8 @@ def html_span(s, aspec, cursor = -1):
         fg_r, fg_g, fg_b = _d_fg_r, _d_fg_g, _d_fg_b
     if bg_r is None:
         bg_r, bg_g, bg_b = _d_bg_r, _d_bg_g, _d_bg_b
-    html_fg = "#%02x%02x%02x" % (fg_r, fg_g, fg_b)
-    html_bg = "#%02x%02x%02x" % (bg_r, bg_g, bg_b)
+    html_fg = f"#{fg_r:02x}{fg_g:02x}{fg_b:02x}"
+    html_bg = f"#{bg_r:02x}{bg_g:02x}{bg_b:02x}"
     if aspec.standout:
         html_fg, html_bg = html_bg, html_fg
     extra = (";text-decoration:underline" * aspec.underline +
diff --git a/urwid/lcd_display.py b/urwid/lcd_display.py
index e189d9a42..9b0a8e8b6 100644
--- a/urwid/lcd_display.py
+++ b/urwid/lcd_display.py
@@ -1,5 +1,4 @@
 #!/usr/bin/python
-# -*- coding: utf-8 -*-
 #
 # Urwid LCD display module
 #    Copyright (C) 2010  Ian Ward
@@ -18,14 +17,16 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
-from .display_common import BaseScreen
+from __future__ import annotations
 
 import time
 
+from .display_common import BaseScreen
+
+
 class LCDScreen(BaseScreen):
     def set_terminal_properties(self, colors=None, bright_is_bold=None,
         has_underline=None):
@@ -93,7 +94,7 @@ def __init__(self, device_path, baud):
         device_path -- eg. '/dev/ttyUSB0'
         baud -- baud rate
         """
-        super(CFLCDScreen, self).__init__()
+        super().__init__()
         self.device_path = device_path
         from serial import Serial
         self._device = Serial(device_path, baud, timeout=0)
@@ -196,7 +197,7 @@ def _parse_data(cls, data):
 
 
 
-class KeyRepeatSimulator(object):
+class KeyRepeatSimulator:
     """
     Provide simulated repeat key events when given press and
     release events.
@@ -252,7 +253,7 @@ def sent_event(self):
 
 
 class CF635Screen(CFLCDScreen):
-    u"""
+    """
     Crystal Fontz 635 display
 
     20x4 character display + cursor
@@ -274,22 +275,22 @@ class CF635Screen(CFLCDScreen):
     #   both groups are intended to draw horizontal bars with pixel
     #   precision, use ▇*[▆▄▃▁]? for a thin bar or ▉*[▋▌▍▏]? for a thick bar
     CGROM = (
-        u"①②③④⑤⑥⑦⑧①②③④⑤⑥⑦⑧"
-        u"►◄⇑⇓«»↖↗↙↘▲▼↲^ˇ█"
-        u" !\"#¤%&'()*+,-./"
-        u"0123456789:;<=>?"
-        u"¡ABCDEFGHIJKLMNO"
-        u"PQRSTUVWXYZÄÖÑÜ§"
-        u"¿abcdefghijklmno"
-        u"pqrstuvwxyzäöñüà"
-        u"⁰¹²³⁴⁵⁶⁷⁸⁹½¼±≥≤μ"
-        u"♪♫⑴♥♦⑵⌜⌟“”()αɛδ∞"
-        u"@£$¥èéùìòÇᴾØøʳÅå"
-        u"⌂¢ΦτλΩπΨΣθΞ♈ÆæßÉ"
-        u"ΓΛΠϒ_ÈÊêçğŞşİι~◊"
-        u"▇▆▄▃▁ƒ▉▋▌▍▏⑶◽▪↑→"
-        u"↓←ÁÍÓÚÝáíóúýÔôŮů"
-        u"ČĔŘŠŽčĕřšž[\]{|}")
+        "①②③④⑤⑥⑦⑧①②③④⑤⑥⑦⑧"
+        "►◄⇑⇓«»↖↗↙↘▲▼↲^ˇ█"
+        " !\"#¤%&'()*+,-./"
+        "0123456789:;<=>?"
+        "¡ABCDEFGHIJKLMNO"
+        "PQRSTUVWXYZÄÖÑÜ§"
+        "¿abcdefghijklmno"
+        "pqrstuvwxyzäöñüà"
+        "⁰¹²³⁴⁵⁶⁷⁸⁹½¼±≥≤μ"
+        "♪♫⑴♥♦⑵⌜⌟“”()αɛδ∞"
+        "@£$¥èéùìòÇᴾØøʳÅå"
+        "⌂¢ΦτλΩπΨΣθΞ♈ÆæßÉ"
+        "ΓΛΠϒ_ÈÊêçğŞşİι~◊"
+        "▇▆▄▃▁ƒ▉▋▌▍▏⑶◽▪↑→"
+        "↓←ÁÍÓÚÝáíóúýÔôŮů"
+        r"ČĔŘŠŽčĕřšž[\]{|}")
 
     cursor_style = CFLCDScreen.CURSOR_INVERTING_BLINKING_BLOCK
 
@@ -303,7 +304,7 @@ def __init__(self, device_path, baud=115200,
         repeat_next -- time between each repeated key
         key_map -- the keys to send for this device's buttons
         """
-        super(CF635Screen, self).__init__(device_path, baud)
+        super().__init__(device_path, baud)
 
         self.repeat_delay = repeat_delay
         self.repeat_next = repeat_next
diff --git a/urwid/listbox.py b/urwid/listbox.py
index e49edaa50..4a5b5ae09 100644
--- a/urwid/listbox.py
+++ b/urwid/listbox.py
@@ -17,26 +17,33 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
-from urwid.compat import xrange, with_metaclass
-from urwid.util import is_mouse_press
-from urwid.canvas import SolidCanvas, CanvasCombine
-from urwid.widget import Widget, nocache_widget_render_instance, BOX, GIVEN
-from urwid.decoration import calculate_top_bottom_filler, normalize_valign
+from __future__ import annotations
+
 from urwid import signals
-from urwid.signals import connect_signal, disconnect_signal
-from urwid.monitored_list import MonitoredList, MonitoredFocusList
+from urwid.canvas import CanvasCombine, SolidCanvas
+from urwid.command_map import (
+    CURSOR_DOWN,
+    CURSOR_MAX_LEFT,
+    CURSOR_MAX_RIGHT,
+    CURSOR_PAGE_DOWN,
+    CURSOR_PAGE_UP,
+    CURSOR_UP,
+)
 from urwid.container import WidgetContainerMixin
-from urwid.command_map import (CURSOR_UP, CURSOR_DOWN,
-    CURSOR_PAGE_UP, CURSOR_PAGE_DOWN, CURSOR_MAX_LEFT, CURSOR_MAX_RIGHT)
+from urwid.decoration import calculate_top_bottom_filler, normalize_valign
+from urwid.monitored_list import MonitoredFocusList, MonitoredList
+from urwid.signals import connect_signal, disconnect_signal
+from urwid.util import is_mouse_press
+from urwid.widget import BOX, GIVEN, Widget, nocache_widget_render_instance
+
 
 class ListWalkerError(Exception):
     pass
 
-class ListWalker(with_metaclass(signals.MetaSignals, object)):
+class ListWalker(metaclass=signals.MetaSignals):
     signals = ["modified"]
 
     def _modified(self):
@@ -96,7 +103,7 @@ def __init__(self, contents, wrap_around=False):
         this list walker to be updated.
         """
         if not getattr(contents, '__getitem__', None):
-            raise ListWalkerError("SimpleListWalker expecting list like object, got: %r"%(contents,))
+            raise ListWalkerError(f"SimpleListWalker expecting list like object, got: {contents!r}")
         MonitoredList.__init__(self, contents)
         self.focus = 0
         self.wrap_around = wrap_around
@@ -131,7 +138,7 @@ def set_focus(self, position):
             if position < 0 or position >= len(self):
                 raise ValueError
         except (TypeError, ValueError):
-            raise IndexError("No widget at position %s" % (position,))
+            raise IndexError(f"No widget at position {position}")
         self.focus = position
         self._modified()
 
@@ -160,8 +167,8 @@ def positions(self, reverse=False):
         Optional method for returning an iterable of positions.
         """
         if reverse:
-            return xrange(len(self) - 1, -1, -1)
-        return xrange(len(self))
+            return range(len(self) - 1, -1, -1)
+        return range(len(self))
 
 
 class SimpleFocusListWalker(ListWalker, MonitoredFocusList):
@@ -228,8 +235,8 @@ def positions(self, reverse=False):
         Optional method for returning an iterable of positions.
         """
         if reverse:
-            return xrange(len(self) - 1, -1, -1)
-        return xrange(len(self))
+            return range(len(self) - 1, -1, -1)
+        return range(len(self))
 
 
 class ListBoxError(Exception):
@@ -466,7 +473,7 @@ def render(self, size, focus=False ):
             raise ListBoxError("Focus Widget %r at position %r within listbox calculated %d rows but rendered %d!"% (focus_widget,focus_pos,focus_rows, focus_canvas.rows()))
         c_cursor = focus_canvas.cursor
         if cursor is not None and cursor != c_cursor:
-            raise ListBoxError("Focus Widget %r at position %r within listbox calculated cursor coords %r but rendered cursor coords %r!" %(focus_widget,focus_pos,cursor,c_cursor))
+            raise ListBoxError(f"Focus Widget {focus_widget!r} at position {focus_pos!r} within listbox calculated cursor coords {cursor!r} but rendered cursor coords {c_cursor!r}!")
 
         rows += focus_rows
         combinelist.append((focus_canvas, focus_pos, True))
@@ -488,13 +495,13 @@ def render(self, size, focus=False ):
             rows -= trim_bottom
 
         if rows > maxrow:
-            raise ListBoxError("Listbox contents too long!  Probably urwid's fault (please report): %r" % ((top,middle,bottom),))
+            raise ListBoxError(f"Listbox contents too long!  Probably urwid's fault (please report): {top, middle, bottom!r}")
 
         if rows < maxrow:
             bottom_pos = focus_pos
             if fill_below: bottom_pos = fill_below[-1][1]
             if trim_bottom != 0 or self._body.get_next(bottom_pos) != (None,None):
-                raise ListBoxError("Listbox contents too short!  Probably urwid's fault (please report): %r" % ((top,middle,bottom),))
+                raise ListBoxError(f"Listbox contents too short!  Probably urwid's fault (please report): {top, middle, bottom!r}")
             final_canvas.pad_trim_top_bottom(0, maxrow - rows)
 
         return final_canvas
@@ -545,8 +552,7 @@ def set_focus(self, position, coming_from=None):
         :type coming_from: str
         """
         if coming_from not in ('above', 'below', None):
-            raise ListBoxError("coming_from value invalid: %r" %
-                (coming_from,))
+            raise ListBoxError(f"coming_from value invalid: {coming_from!r}")
         focus_widget, focus_pos = self._body.get_focus()
         if focus_widget is None:
             raise IndexError("Can't set focus, ListBox is empty")
@@ -588,7 +594,7 @@ def _get_focus_position(self):
         """)
 
     def _contents(self):
-        class ListBoxContents(object):
+        class ListBoxContents:
             __getitem__ = self._contents__getitem__
         return ListBoxContents()
     def _contents__getitem__(self, key):
@@ -598,7 +604,7 @@ def _contents__getitem__(self, key):
             try:
                 return (getitem(key), None)
             except (IndexError, KeyError):
-                raise KeyError("ListBox.contents key not found: %r" % (key,))
+                raise KeyError(f"ListBox.contents key not found: {key!r}")
         # fall back to v1
         w, old_focus = self._body.get_focus()
         try:
@@ -606,10 +612,13 @@ def _contents__getitem__(self, key):
                 self._body.set_focus(key)
                 return self._body.get_focus()[0]
             except (IndexError, KeyError):
-                raise KeyError("ListBox.contents key not found: %r" % (key,))
+                raise KeyError(f"ListBox.contents key not found: {key!r}")
         finally:
             self._body.set_focus(old_focus)
-    contents = property(lambda self: self._contents, doc="""
+
+    @property
+    def contents(self):
+        """
         An object that allows reading widgets from the ListBox's list
         walker as a `(widget, options)` tuple. `None` is currently the only
         value for options.
@@ -620,7 +629,8 @@ def _contents__getitem__(self, key):
 
             You must use the list walker stored as
             :attr:`.body` to perform manipulation and iteration, if supported.
-        """)
+        """
+        return self._contents
 
     def options(self):
         """
@@ -760,14 +770,14 @@ def shift_focus(self, size, offset_inset):
 
         if offset_inset >= 0:
             if offset_inset >= maxrow:
-                raise ListBoxError("Invalid offset_inset: %r, only %r rows in list box"% (offset_inset, maxrow))
+                raise ListBoxError(f"Invalid offset_inset: {offset_inset!r}, only {maxrow!r} rows in list box")
             self.offset_rows = offset_inset
             self.inset_fraction = (0,1)
         else:
             target, _ignore = self._body.get_focus()
             tgt_rows = target.rows( (maxcol,), True )
             if offset_inset + tgt_rows <= 0:
-                raise ListBoxError("Invalid offset_inset: %r, only %r rows in target!" %(offset_inset, tgt_rows))
+                raise ListBoxError(f"Invalid offset_inset: {offset_inset!r}, only {tgt_rows!r} rows in target!")
             self.offset_rows = 0
             self.inset_fraction = (-offset_inset,tgt_rows)
         self._invalidate()
@@ -864,7 +874,7 @@ def change_focus(self, size, position,
             self.inset_fraction = (0,1)
         else:
             if offset_inset + tgt_rows <= 0:
-                raise ListBoxError("Invalid offset_inset: %s, only %s rows in target!" %(offset_inset, tgt_rows))
+                raise ListBoxError(f"Invalid offset_inset: {offset_inset}, only {tgt_rows} rows in target!")
             self.offset_rows = 0
             self.inset_fraction = (-offset_inset,tgt_rows)
 
@@ -892,7 +902,7 @@ def change_focus(self, size, position,
             # start from preferred row and move back to closest edge
             (pref_col, pref_row) = cursor_coords
             if pref_row < 0 or pref_row >= tgt_rows:
-                raise ListBoxError("cursor_coords row outside valid range for target. pref_row:%r target_rows:%r"%(pref_row,tgt_rows))
+                raise ListBoxError(f"cursor_coords row outside valid range for target. pref_row:{pref_row!r} target_rows:{tgt_rows!r}")
 
             if coming_from=='above':
                 attempt_rows = range( pref_row, -1, -1 )
@@ -915,7 +925,7 @@ def get_focus_offset_inset(self, size):
         if offset_rows == 0:
             inum, iden = self.inset_fraction
             if inum < 0 or iden < 0 or inum >= iden:
-                raise ListBoxError("Invalid inset_fraction: %r"%(self.inset_fraction,))
+                raise ListBoxError(f"Invalid inset_fraction: {self.inset_fraction!r}")
             inset_rows = focus_rows * inum // iden
             if inset_rows and inset_rows >= focus_rows:
                 raise ListBoxError("urwid inset_fraction error (please report)")
@@ -1247,8 +1257,8 @@ def _keypress_page_up(self, size):
 
         # choose the topmost selectable and (newly) visible widget
         # search within snap_rows then visible region
-        search_order = (list(xrange(snap_region_start, len(t)))
-            + list(xrange(snap_region_start-1, -1, -1)))
+        search_order = (list(range(snap_region_start, len(t)))
+            + list(range(snap_region_start-1, -1, -1)))
         #assert 0, repr((t, search_order))
         bad_choices = []
         cut_off_selectable_chosen = 0
@@ -1432,8 +1442,8 @@ def _keypress_page_down(self, size):
 
         # choose the bottommost selectable and (newly) visible widget
         # search within snap_rows then visible region
-        search_order = (list(xrange(snap_region_start, len(t)))
-            + list(xrange(snap_region_start-1, -1, -1)))
+        search_order = (list(range(snap_region_start, len(t)))
+            + list(range(snap_region_start-1, -1, -1)))
         #assert 0, repr((t, search_order))
         bad_choices = []
         cut_off_selectable_chosen = 0
diff --git a/urwid/main_loop.py b/urwid/main_loop.py
index 514950534..0d91a6037 100755
--- a/urwid/main_loop.py
+++ b/urwid/main_loop.py
@@ -19,16 +19,17 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
-import time
+from __future__ import annotations
+
 import heapq
-import select
 import os
+import select
 import signal
 import sys
+import time
 from functools import wraps
 from itertools import count
 from weakref import WeakKeyDictionary
@@ -38,12 +39,12 @@
 except ImportError:
     pass # windows
 
-from urwid.util import StoppingContext, is_mouse_event
-from urwid.compat import PYTHON3, reraise
-from urwid.command_map import command_map, REDRAW_SCREEN
-from urwid.wimp import PopUpTarget
 from urwid import signals
+from urwid.command_map import REDRAW_SCREEN, command_map
+from urwid.compat import reraise
 from urwid.display_common import INPUT_DESCRIPTORS_CHANGED
+from urwid.util import StoppingContext, is_mouse_event
+from urwid.wimp import PopUpTarget
 
 PIPE_BUFFER_READ_SIZE = 4096 # can expect this much on Linux, so try for that
 
@@ -57,7 +58,7 @@ class ExitMainLoop(Exception):
 class CantUseExternalLoop(Exception):
     pass
 
-class MainLoop(object):
+class MainLoop:
     """
     This is the standard main loop implementation for a single interactive
     session.
@@ -589,7 +590,7 @@ def draw_screen(self):
         self.screen.draw_screen(self.screen_size, canvas)
 
 
-class EventLoop(object):
+class EventLoop:
     """
     Abstract class representing an event loop to be used by :class:`MainLoop`.
     """
@@ -788,7 +789,7 @@ def run(self):
             while True:
                 try:
                     self._loop()
-                except select.error as e:
+                except OSError as e:
                     if e.args[0] != 4:
                         # not just something we need to retry
                         raise
@@ -1044,7 +1045,7 @@ class TornadoEventLoop(EventLoop):
     _ioloop_registry = WeakKeyDictionary()  # {<ioloop> : {<handle> : <idle_func>}}
     _max_idle_handle = 0
 
-    class PollProxy(object):
+    class PollProxy:
         """ A simple proxy for a Python's poll object that wraps the .poll() method
             in order to detect idle periods and call Urwid callbacks
         """
@@ -1217,7 +1218,7 @@ def __init__(self, reactor=None, manage_reactor=True):
            instead call start() and stop() before and after starting the
            reactor.
 
-        .. _Twisted: http://twistedmatrix.com/trac/
+        .. _Twisted: https://twisted.org/
         """
         if reactor is None:
             import twisted.internet.reactor
@@ -1251,7 +1252,7 @@ def remove_alarm(self, handle):
 
         Returns True if the alarm exists, False otherwise
         """
-        from twisted.internet.error import AlreadyCancelled, AlreadyCalled
+        from twisted.internet.error import AlreadyCalled, AlreadyCancelled
         try:
             handle.cancel()
             return True
@@ -1496,10 +1497,9 @@ def run(self):
 
 # Import Trio's event loop only if we are on Python 3.5 or above (async def is
 # not supported in earlier versions).
-if sys.version_info >= (3, 5):
-    from ._async_kw_event_loop import TrioEventLoop
+from ._async_kw_event_loop import TrioEventLoop
+
 
- 
 def _refl(name, rval=None, exit=False):
     """
     This function is used to test the main loop classes.
@@ -1516,26 +1516,26 @@ def _refl(name, rval=None, exit=False):
     42
 
     """
-    class Reflect(object):
+    class Reflect:
         def __init__(self, name, rval=None):
             self._name = name
             self._rval = rval
         def __call__(self, *argl, **argd):
             args = ", ".join([repr(a) for a in argl])
             if args and argd:
-                args = args + ", "
-            args = args + ", ".join([k+"="+repr(v) for k,v in argd.items()])
-            print(self._name+"("+args+")")
+                args = f"{args}, "
+            args = args + ", ".join([f"{k}={repr(v)}" for k,v in argd.items()])
+            print(f"{self._name}({args})")
             if exit:
                 raise ExitMainLoop()
             return self._rval
         def __getattr__(self, attr):
             if attr.endswith("_rval"):
                 raise AttributeError()
-            #print self._name+"."+attr
-            if hasattr(self, attr+"_rval"):
-                return Reflect(self._name+"."+attr, getattr(self, attr+"_rval"))
-            return Reflect(self._name+"."+attr)
+            #print(self._name+"."+attr)
+            if hasattr(self, f"{attr}_rval"):
+                return Reflect(f"{self._name}.{attr}", getattr(self, f"{attr}_rval"))
+            return Reflect(f"{self._name}.{attr}")
     return Reflect(name)
 
 def _test():
diff --git a/urwid/monitored_list.py b/urwid/monitored_list.py
index c15906409..8a0461559 100755
--- a/urwid/monitored_list.py
+++ b/urwid/monitored_list.py
@@ -17,11 +17,7 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
-
-from __future__ import division, print_function
-
-from urwid.compat import PYTHON3, xrange
+# Urwid web site: https://urwid.org/
 
 
 def _call_modified(fn):
@@ -63,18 +59,16 @@ def set_modified_callback(self, callback):
         self._modified = callback
 
     def __repr__(self):
-        return "%s(%r)" % (self.__class__.__name__, list(self))
+        return f"{self.__class__.__name__}({list(self)!r})"
 
     __add__ = _call_modified(list.__add__)
     __delitem__ = _call_modified(list.__delitem__)
-    if not PYTHON3:
-        __delslice__ = _call_modified(list.__delslice__)
+
     __iadd__ = _call_modified(list.__iadd__)
     __imul__ = _call_modified(list.__imul__)
     __rmul__ = _call_modified(list.__rmul__)
     __setitem__ = _call_modified(list.__setitem__)
-    if not PYTHON3:
-        __setslice__ = _call_modified(list.__setslice__)
+
     append = _call_modified(list.append)
     extend = _call_modified(list.extend)
     insert = _call_modified(list.insert)
@@ -114,14 +108,13 @@ def __init__(self, *argl, **argd):
         """
         focus = argd.pop('focus', 0)
 
-        super(MonitoredFocusList, self).__init__(*argl, **argd)
+        super().__init__(*argl, **argd)
 
         self._focus = focus
         self._focus_modified = lambda ml, indices, new_items: None
 
     def __repr__(self):
-        return "%s(%r, focus=%r)" % (
-            self.__class__.__name__, list(self), self.focus)
+        return f"{self.__class__.__name__}({list(self)!r}, focus={self.focus!r})"
 
     def _get_focus(self):
         """
@@ -162,9 +155,9 @@ def _set_focus(self, index):
             self._focus = 0
             return
         if index < 0 or index >= len(self):
-            raise IndexError('focus index is out of range: %s' % (index,))
+            raise IndexError(f'focus index is out of range: {index}')
         if index != int(index):
-            raise IndexError('invalid focus index: %s' % (index,))
+            raise IndexError(f'invalid focus index: {index}')
         index = int(index)
         if index != self._focus:
             self._focus_changed(index)
@@ -240,7 +233,7 @@ def _adjust_focus_on_contents_modified(self, slc, new_items=()):
         """
         num_new_items = len(new_items)
         start, stop, step = indices = slc.indices(len(self))
-        num_removed = len(list(xrange(*indices)))
+        num_removed = len(list(range(*indices)))
 
         focus = self._validate_contents_modified(indices, new_items)
         if focus is not None:
@@ -257,11 +250,11 @@ def _adjust_focus_on_contents_modified(self, slc, new_items=()):
         else:
             if not num_new_items:
                 # extended slice being removed
-                if focus in xrange(start, stop, step):
+                if focus in range(start, stop, step):
                     focus += 1
 
                 # adjust for removed items
-                focus -= len(list(xrange(start, min(focus, stop), step)))
+                focus -= len(list(range(start, min(focus, stop), step)))
 
         return min(focus, len(self) + num_new_items - num_removed -1)
 
@@ -298,7 +291,7 @@ def __delitem__(self, y):
         else:
             focus = self._adjust_focus_on_contents_modified(slice(y,
                 y+1 or None))
-        rval = super(MonitoredFocusList, self).__delitem__(y)
+        rval = super().__delitem__(y)
         self._set_focus(focus)
         return rval
 
@@ -335,17 +328,10 @@ def __setitem__(self, i, y):
             focus = self._adjust_focus_on_contents_modified(i, y)
         else:
             focus = self._adjust_focus_on_contents_modified(slice(i, i+1 or None), [y])
-        rval = super(MonitoredFocusList, self).__setitem__(i, y)
+        rval = super().__setitem__(i, y)
         self._set_focus(focus)
         return rval
 
-    if not PYTHON3:
-        def __delslice__(self, i, j):
-            return self.__delitem__(slice(i,j))
-
-        def __setslice__(self, i, j, y):
-            return self.__setitem__(slice(i, j), y)
-
     def __imul__(self, n):
         """
         >>> def modified(indices, new_items):
@@ -366,7 +352,7 @@ def __imul__(self, n):
                 slice(len(self), len(self)), list(self)*(n-1))
         else: # all contents are being removed
             focus = self._adjust_focus_on_contents_modified(slice(0, len(self)))
-        rval = super(MonitoredFocusList, self).__imul__(n)
+        rval = super().__imul__(n)
         self._set_focus(focus)
         return rval
 
@@ -381,7 +367,7 @@ def append(self, item):
         """
         focus = self._adjust_focus_on_contents_modified(
             slice(len(self), len(self)), [item])
-        rval = super(MonitoredFocusList, self).append(item)
+        rval = super().append(item)
         self._set_focus(focus)
         return rval
 
@@ -396,7 +382,7 @@ def extend(self, items):
         """
         focus = self._adjust_focus_on_contents_modified(
             slice(len(self), len(self)), items)
-        rval = super(MonitoredFocusList, self).extend(items)
+        rval = super().extend(items)
         self._set_focus(focus)
         return rval
 
@@ -412,7 +398,7 @@ def insert(self, index, item):
         """
         focus = self._adjust_focus_on_contents_modified(slice(index, index),
             [item])
-        rval = super(MonitoredFocusList, self).insert(index, item)
+        rval = super().insert(index, item)
         self._set_focus(focus)
         return rval
 
@@ -434,7 +420,7 @@ def pop(self, index=-1):
         """
         focus = self._adjust_focus_on_contents_modified(slice(index,
             index+1 or None))
-        rval = super(MonitoredFocusList, self).pop(index)
+        rval = super().pop(index)
         self._set_focus(focus)
         return rval
 
@@ -451,7 +437,7 @@ def remove(self, value):
         index = self.index(value)
         focus = self._adjust_focus_on_contents_modified(slice(index,
             index+1 or None))
-        rval = super(MonitoredFocusList, self).remove(value)
+        rval = super().remove(value)
         self._set_focus(focus)
         return rval
 
@@ -461,7 +447,7 @@ def reverse(self):
         >>> ml.reverse(); ml
         MonitoredFocusList([4, 3, 2, 1, 0], focus=3)
         """
-        rval = super(MonitoredFocusList, self).reverse()
+        rval = super().reverse()
         self._set_focus(max(0, len(self) - self._focus - 1))
         return rval
 
@@ -474,14 +460,14 @@ def sort(self, **kwargs):
         if not self:
             return
         value = self[self._focus]
-        rval = super(MonitoredFocusList, self).sort(**kwargs)
+        rval = super().sort(**kwargs)
         self._set_focus(self.index(value))
         return rval
 
     if hasattr(list, 'clear'):
         def clear(self):
             focus = self._adjust_focus_on_contents_modified(slice(0, 0))
-            rval = super(MonitoredFocusList, self).clear()
+            rval = super().clear()
             self._set_focus(focus)
             return rval
 
diff --git a/urwid/numedit.py b/urwid/numedit.py
index 3e073cfad..ff372d52c 100644
--- a/urwid/numedit.py
+++ b/urwid/numedit.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
 #
 # Urwid basic widget classes
 #    Copyright (C) 2004-2012  Ian Ward
@@ -17,12 +16,15 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
 
-from urwid import Edit
-from decimal import Decimal
+from __future__ import annotations
+
 import re
+from decimal import Decimal
+
+from urwid import Edit
 
 
 class NumEdit(Edit):
@@ -38,7 +40,7 @@ class NumEdit(Edit):
     ALLOWED = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
 
     def __init__(self, allowed, caption, default, trimLeadingZeros=True):
-        super(NumEdit, self).__init__(caption, default)
+        super().__init__(caption, default)
         self._allowed = allowed
         self.trimLeadingZeros = trimLeadingZeros
 
@@ -149,10 +151,9 @@ def __init__(self, caption="", default=None, base=10):
             # in case a float is passed or some other error
             if isinstance(default, str) and len(default):
                 # check if it is a valid initial value
-                validation_re = "^[{}]+$".format(allowed_chars)
+                validation_re = f"^[{allowed_chars}]+$"
                 if not re.match(validation_re, str(default), re.IGNORECASE):
-                    raise ValueError("invalid value: {} for base {}".format(
-                                     default, base))
+                    raise ValueError(f"invalid value: {default} for base {base}")
 
             elif isinstance(default, Decimal):
                 # a Decimal instance with no fractional part
@@ -162,7 +163,7 @@ def __init__(self, caption="", default=None, base=10):
             # convert possible int, long or Decimal to str
             val = str(default)
 
-        super(IntegerEdit, self).__init__(allowed_chars, caption, val,
+        super().__init__(allowed_chars, caption, val,
                                           trimLeadingZeros=(self.base == 10))
 
     def value(self):
@@ -238,8 +239,7 @@ def __init__(self, caption="", default=None,
         self.significance = None
         self._decimalSeparator = decimalSeparator
         if decimalSeparator not in ['.', ',']:
-            raise ValueError("invalid decimalSeparator: {}".format(
-                             decimalSeparator))
+            raise ValueError(f"invalid decimalSeparator: {decimalSeparator}")
 
         val = ""
         if default is not None and default != "":
@@ -257,7 +257,7 @@ def __init__(self, caption="", default=None,
 
             val = str(default)
 
-        super(FloatEdit, self).__init__(self.ALLOWED[0:10] + decimalSeparator,
+        super().__init__(self.ALLOWED[0:10] + decimalSeparator,
                                         caption, val)
 
     def value(self):
diff --git a/urwid/old_str_util.py b/urwid/old_str_util.py
index 4d3461d72..920b4a868 100755
--- a/urwid/old_str_util.py
+++ b/urwid/old_str_util.py
@@ -1,5 +1,4 @@
 #!/usr/bin/python
-# -*- coding: utf-8 -*-
 #
 # Urwid unicode character processing tables
 #    Copyright (C) 2004-2011  Ian Ward
@@ -18,16 +17,15 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
-import re
+from __future__ import annotations
 
-from urwid.compat import B, ord2, text_type
+import re
 
-SAFE_ASCII_RE = re.compile(u"^[ -~]*$")
-SAFE_ASCII_BYTES_RE = re.compile(B("^[ -~]*$"))
+SAFE_ASCII_RE = re.compile("^[ -~]*$")
+SAFE_ASCII_BYTES_RE = re.compile(b"^[ -~]*$")
 
 _byte_encoding = None
 
@@ -93,7 +91,7 @@ def decode_one( text, pos ):
     Return (ordinal at pos, next position) for UTF-8 encoded text.
     """
     assert isinstance(text, bytes), text
-    b1 = ord2(text[pos])
+    b1 = text[pos]
     if not b1 & 0x80:
         return b1, pos+1
     error = ord("?"), pos+1
@@ -102,7 +100,7 @@ def decode_one( text, pos ):
     if lt < 2:
         return error
     if b1 & 0xe0 == 0xc0:
-        b2 = ord2(text[pos+1])
+        b2 = text[pos+1]
         if b2 & 0xc0 != 0x80:
             return error
         o = ((b1&0x1f)<<6)|(b2&0x3f)
@@ -112,10 +110,10 @@ def decode_one( text, pos ):
     if lt < 3:
         return error
     if b1 & 0xf0 == 0xe0:
-        b2 = ord2(text[pos+1])
+        b2 = text[pos+1]
         if b2 & 0xc0 != 0x80:
             return error
-        b3 = ord2(text[pos+2])
+        b3 = text[pos+2]
         if b3 & 0xc0 != 0x80:
             return error
         o = ((b1&0x0f)<<12)|((b2&0x3f)<<6)|(b3&0x3f)
@@ -125,13 +123,13 @@ def decode_one( text, pos ):
     if lt < 4:
         return error
     if b1 & 0xf8 == 0xf0:
-        b2 = ord2(text[pos+1])
+        b2 = text[pos+1]
         if b2 & 0xc0 != 0x80:
             return error
-        b3 = ord2(text[pos+2])
+        b3 = text[pos+2]
         if b3 & 0xc0 != 0x80:
             return error
-        b4 = ord2(text[pos+2])
+        b4 = text[pos+2]
         if b4 & 0xc0 != 0x80:
             return error
         o = ((b1&0x07)<<18)|((b2&0x3f)<<12)|((b3&0x3f)<<6)|(b4&0x3f)
@@ -155,7 +153,7 @@ def decode_one_right(text, pos):
     error = ord("?"), pos-1
     p = pos
     while p >= 0:
-        if ord2(text[p])&0xc0 != 0x80:
+        if text[p]&0xc0 != 0x80:
             o, next = decode_one( text, p )
             return o, p-1
         p -=1
@@ -242,7 +240,7 @@ def is_wide_char(text, offs):
 
     text may be unicode or a byte string in the target _byte_encoding
     """
-    if isinstance(text, text_type):
+    if isinstance(text, str):
         o = ord(text[offs])
         return get_width(o) == 2
     assert isinstance(text, bytes)
@@ -258,12 +256,12 @@ def move_prev_char(text, start_offs, end_offs):
     Return the position of the character before end_offs.
     """
     assert start_offs < end_offs
-    if isinstance(text, text_type):
+    if isinstance(text, str):
         return end_offs-1
     assert isinstance(text, bytes)
     if _byte_encoding == "utf8":
         o = end_offs-1
-        while ord2(text[o])&0xc0 == 0x80:
+        while text[o]&0xc0 == 0x80:
             o -= 1
         return o
     if _byte_encoding == "wide" and within_double_byte(text,
@@ -276,12 +274,12 @@ def move_next_char(text, start_offs, end_offs):
     Return the position of the character after start_offs.
     """
     assert start_offs < end_offs
-    if isinstance(text, text_type):
+    if isinstance(text, str):
         return start_offs+1
     assert isinstance(text, bytes)
     if _byte_encoding == "utf8":
         o = start_offs+1
-        while o<end_offs and ord2(text[o])&0xc0 == 0x80:
+        while o<end_offs and text[o]&0xc0 == 0x80:
             o += 1
         return o
     if _byte_encoding == "wide" and within_double_byte(text,
@@ -302,13 +300,13 @@ def within_double_byte(text, line_start, pos):
     2 -- pos is on the 2nd half of a dbe char
     """
     assert isinstance(text, bytes)
-    v = ord2(text[pos])
+    v = text[pos]
 
     if v >= 0x40 and v < 0x7f:
         # might be second half of big5, uhc or gbk encoding
         if pos == line_start: return 0
 
-        if ord2(text[pos-1]) >= 0x81:
+        if text[pos-1] >= 0x81:
             if within_double_byte(text, line_start, pos-1) == 1:
                 return 2
         return 0
@@ -317,7 +315,7 @@ def within_double_byte(text, line_start, pos):
 
     i = pos -1
     while i >= line_start:
-        if ord2(text[i]) < 0x80:
+        if text[i] < 0x80:
             break
         i -= 1
 
@@ -361,7 +359,7 @@ def process_east_asian_width():
 
     print("widths = [")
     for o in out[1:]:  # treat control characters same as ascii
-        print("\t%r," % (o,))
+        print(f"\t{o!r},")
     print("]")
 
 if __name__ == "__main__":
diff --git a/urwid/raw_display.py b/urwid/raw_display.py
index db470a544..f24e30b42 100644
--- a/urwid/raw_display.py
+++ b/urwid/raw_display.py
@@ -17,19 +17,20 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
 """
 Direct terminal UI implementation
 """
 
+from __future__ import annotations
+
 import os
 import select
+import signal
 import struct
 import sys
-import signal
 
 try:
     import fcntl
@@ -38,15 +39,17 @@
 except ImportError:
     pass # windows
 
-from urwid import util
-from urwid import escape
-from urwid.display_common import BaseScreen, RealTerminal, \
-    UPDATE_PALETTE_ENTRY, AttrSpec, UNPRINTABLE_TRANS_TABLE, \
-    INPUT_DESCRIPTORS_CHANGED
-from urwid import signals
-from urwid.compat import PYTHON3, B
+from subprocess import PIPE, Popen
 
-from subprocess import Popen, PIPE
+from urwid import escape, signals, util
+from urwid.display_common import (
+    INPUT_DESCRIPTORS_CHANGED,
+    UNPRINTABLE_TRANS_TABLE,
+    UPDATE_PALETTE_ENTRY,
+    AttrSpec,
+    BaseScreen,
+    RealTerminal,
+)
 
 
 class Screen(BaseScreen, RealTerminal):
@@ -54,7 +57,7 @@ def __init__(self, input=sys.stdin, output=sys.stdout):
         """Initialize a screen that directly prints escape codes to an output
         terminal.
         """
-        super(Screen, self).__init__()
+        super().__init__()
         self._pal_escape = {}
         self._pal_attrspec = {}
         signals.connect_signal(self, UPDATE_PALETTE_ENTRY,
@@ -136,7 +139,7 @@ def _sigwinch_handler(self, signum, frame=None):
         """
 
         if not self._resized:
-            os.write(self._resize_pipe_wr, B('R'))
+            os.write(self._resize_pipe_wr, b'R')
         self._resized = True
         self.screen_buf = None
 
@@ -246,7 +249,7 @@ def _start(self, alternate_buffer=True):
         # restore mouse tracking to previous state
         self._mouse_tracking(self._mouse_tracking_enabled)
 
-        return super(Screen, self)._start()
+        return super()._start()
 
     def _stop(self):
         """
@@ -280,7 +283,7 @@ def _stop(self):
         if self._old_signal_keys:
             self.tty_signal_keys(*(self._old_signal_keys + (fd,)))
 
-        super(Screen, self)._stop()
+        super()._stop()
 
 
     def write(self, data):
@@ -537,7 +540,7 @@ def _get_gpm_codes(self):
         try:
             while self.gpm_mev is not None and self.gpm_event_pending:
                 codes.extend(self._encode_gpm_event())
-        except IOError as e:
+        except OSError as e:
             if e.args[0] != 11:
                 raise
         return codes
@@ -559,7 +562,7 @@ def _wait_for_input_ready(self, timeout):
                     ready,w,err = select.select(
                         fd_list,[],fd_list, timeout)
                 break
-            except select.error as e:
+            except OSError as e:
                 if e.args[0] != 4:
                     raise
                 if self._resized:
@@ -675,7 +678,7 @@ def get_cols_rows(self):
                 buf = fcntl.ioctl(self._term_output_file.fileno(),
                                 termios.TIOCGWINSZ, ' '*4)
                 y, x = struct.unpack('hh', buf)
-        except IOError:
+        except OSError:
             # Term size could not be determined
             pass
         self.maxrow = y
@@ -693,7 +696,7 @@ def _setup_G1(self):
                 self.write(escape.DESIGNATE_G1_SPECIAL)
                 self.flush()
                 break
-            except IOError:
+            except OSError:
                 pass
         self._setup_G1_done = True
 
@@ -808,10 +811,10 @@ def using_standout_or_underline(a):
             whitespace_at_end = False
             if row:
                 a, cs, run = row[-1]
-                if (run[-1:] == B(' ') and self.back_color_erase
+                if (run[-1:] == b' ' and self.back_color_erase
                         and not using_standout_or_underline(a)):
                     whitespace_at_end = True
-                    row = row[:-1] + [(a, cs, run.rstrip(B(' ')))]
+                    row = row[:-1] + [(a, cs, run.rstrip(b' '))]
                 elif y == maxrow-1 and maxcol > 1:
                     row, back, ins = self._last_row(row)
 
@@ -869,11 +872,11 @@ def using_standout_or_underline(a):
             return
         try:
             for l in o:
-                if isinstance(l, bytes) and PYTHON3:
+                if isinstance(l, bytes):
                     l = l.decode('utf-8', 'replace')
                 self.write(l)
             self.flush()
-        except IOError as e:
+        except OSError as e:
             # ignore interrupted syscall
             if e.args[0] != 4:
                 raise
@@ -987,7 +990,7 @@ def _attrspec_to_escape(self, a):
                 bg = "%d" % (a.background_number + 40)
         else:
             bg = "49"
-        return escape.ESC + "[0;%s;%s%sm" % (fg, st, bg)
+        return f"{escape.ESC}[0;{fg};{st}{bg}m"
 
 
     def set_terminal_properties(self, colors=None, bright_is_bold=None,
@@ -1064,11 +1067,11 @@ def modify_terminal_palette(self, entries):
         if self.term == 'fbterm':
             modify = ["%d;%d;%d;%d" % (index, red, green, blue)
                 for index, red, green, blue in entries]
-            self.write("\x1b[3;"+";".join(modify)+"}")
+            self.write(f"\x1b[3;{';'.join(modify)}}}")
         else:
             modify = ["%d;rgb:%02x/%02x/%02x" % (index, red, green, blue)
                 for index, red, green, blue in entries]
-            self.write("\x1b]4;"+";".join(modify)+"\x1b\\")
+            self.write(f"\x1b]4;{';'.join(modify)}\x1b\\")
         self.flush()
 
 
diff --git a/urwid/signals.py b/urwid/signals.py
index 78f1eaec0..ea401314d 100644
--- a/urwid/signals.py
+++ b/urwid/signals.py
@@ -17,11 +17,13 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
+
+from __future__ import annotations
 
 import itertools
+import warnings
 import weakref
 
 
@@ -34,10 +36,10 @@ def __init__(cls, name, bases, d):
         signals = d.get("signals", [])
         for superclass in cls.__bases__:
             signals.extend(getattr(superclass, 'signals', []))
-        signals = list(dict([(x,None) for x in signals]).keys())
+        signals = list({x:None for x in signals}.keys())
         d["signals"] = signals
         register_signal(cls, signals)
-        super(MetaSignals, cls).__init__(name, bases, d)
+        super().__init__(name, bases, d)
 
 def setdefaultattr(obj, name, value):
     # like dict.setdefault() for object attributes
@@ -46,14 +48,14 @@ def setdefaultattr(obj, name, value):
     setattr(obj, name, value)
     return value
 
-class Key(object):
+class Key:
     """
     Minimal class, whose only purpose is to produce objects with a
     unique hash
     """
     __slots__ = []
 
-class Signals(object):
+class Signals:
     _signal_attr = '_urwid_signals' # attribute to attach to signal senders
 
     def __init__(self):
@@ -148,11 +150,15 @@ def connect(self, obj, name, callback, user_arg=None, weak_args=None, user_args=
         handler can also be disconnected by calling
         urwid.disconnect_signal, which doesn't need this key.
         """
+        if user_arg is not None:
+            warnings.warn(
+                "Don't use user_arg argument, use user_args instead.",
+                DeprecationWarning,
+            )
 
         sig_cls = obj.__class__
         if not name in self._supported.get(sig_cls, []):
-            raise NameError("No such signal %r for object %r" %
-                (name, obj))
+            raise NameError(f"No such signal {name!r} for object {obj!r}")
 
         # Just generate an arbitrary (but unique) key
         key = Key()
@@ -247,8 +253,7 @@ def emit(self, obj, name, *args):
         :type obj: object
         :param name: the signal to send, typically a string
         :type name: signal name
-        :param \*args: zero or more positional arguments to pass to the signal
-                      callback functions
+        :param args: zero or more positional arguments to pass to the signal callback functions
 
         This function calls each of the callbacks connected to this signal
         with the args arguments as positional parameters.
diff --git a/urwid/split_repr.py b/urwid/split_repr.py
index 5edf4c937..3b27a4a68 100755
--- a/urwid/split_repr.py
+++ b/urwid/split_repr.py
@@ -17,15 +17,13 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
-from urwid.compat import PYTHON3
-if not PYTHON3:
-    from inspect import getargspec
-else:
-    from inspect import getfullargspec
+from __future__ import annotations
+
+from inspect import getfullargspec
+
 
 def split_repr(self):
     """
@@ -50,9 +48,8 @@ def split_repr(self):
     >>> Bar()
     <Bar words here too attrs='appear too' barttr=42>
     """
-    alist = [(str(k), normalize_repr(v))
-        for k, v in self._repr_attrs().items()]
-    alist.sort()
+    alist = sorted((str(k), normalize_repr(v)) for k, v in self._repr_attrs().items())
+
     words = self._repr_words()
     if not words and not alist:
         # if we're just going to print the classname fall back
@@ -73,30 +70,11 @@ def normalize_repr(v):
     "'foo'"
     """
     if isinstance(v, dict):
-        items = [(repr(k), repr(v)) for k, v in v.items()]
-        items.sort()
-        return "{" + ", ".join([
-            "%s: %s" % itm for itm in items]) + "}"
+        items = sorted((repr(k), repr(v)) for k, v in v.items())
 
-    return repr(v)
-
-def python3_repr(v):
-    """
-    Return strings and byte strings as they appear in Python 3
-
-    >>> python3_repr(u"text")
-    "'text'"
-    >>> python3_repr(bytes())
-    "b''"
-    """
-    r = repr(v)
-    if not PYTHON3:
-        if isinstance(v, bytes):
-            return 'b' + r
-        if r.startswith('u'):
-            return r[1:]
-    return r
+        return f"{{{', '.join([('%s: %s' % itm) for itm in items])}}}"
 
+    return repr(v)
 
 
 def remove_defaults(d, fn):
@@ -125,10 +103,7 @@ def remove_defaults(d, fn):
     >>> Foo()
     <Foo object>
     """
-    if not PYTHON3:
-        args, varargs, varkw, defaults = getargspec(fn)
-    else:
-        args, varargs, varkw, defaults, _, _, _ = getfullargspec(fn)
+    args, varargs, varkw, defaults, _, _, _ = getfullargspec(fn)
 
     # ignore *varargs and **kwargs
     if varkw:
diff --git a/urwid/text_layout.py b/urwid/text_layout.py
index a1c64f401..5fcb40762 100644
--- a/urwid/text_layout.py
+++ b/urwid/text_layout.py
@@ -1,5 +1,4 @@
 #!/usr/bin/python
-# -*- coding: utf-8 -*-
 #
 # Urwid Text Layout classes
 #    Copyright (C) 2004-2011  Ian Ward
@@ -18,13 +17,20 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
-from urwid.util import calc_width, calc_text_pos, calc_trim_text, is_wide_char, \
-    move_prev_char, move_next_char
-from urwid.compat import PYTHON3, B, xrange
+from __future__ import annotations
+
+from urwid.util import (
+    calc_text_pos,
+    calc_trim_text,
+    calc_width,
+    is_wide_char,
+    move_next_char,
+    move_prev_char,
+)
+
 
 class TextLayout:
     def supports_align_mode(self, align):
@@ -94,7 +100,7 @@ def pack(self, maxcol, layout):
         returned by self.layout().
         """
         maxwidth = 0
-        assert layout, "huh? empty layout?: "+repr(layout)
+        assert layout, f"huh? empty layout?: {layout!r}"
         for l in layout:
             lw = line_width(l)
             if lw >= maxcol:
@@ -131,8 +137,8 @@ def calculate_text_segments(self, text, width, wrap):
         Returns a layout structure without alignment applied.
         """
         nl, nl_o, sp_o = "\n", "\n", " "
-        if PYTHON3 and isinstance(text, bytes):
-            nl = B(nl) # can only find bytes in python3 bytestrings
+        if isinstance(text, bytes):
+            nl = nl.encode('iso8859-1') # can only find bytes in python3 bytestrings
             nl_o = ord(nl_o) # + an item of a bytestring is the ordinal value
             sp_o = ord(sp_o)
         b = []
@@ -163,7 +169,7 @@ def calculate_text_segments(self, text, width, wrap):
                 if p!=n_end:
                     l += [(sc, p, n_end)]
                 if trimmed:
-                    l += [(1, n_end, u'…'.encode("utf-8"))]
+                    l += [(1, n_end, '…'.encode())]
                 l += [(pad_right,n_end)]
                 b.append(l)
                 p = n_cr+1
@@ -318,8 +324,8 @@ def subseg(self, text, start, end):
             # use text stored in segment (self.text)
             spos, epos, pad_left, pad_right = calc_trim_text(
                 self.text, 0, len(self.text), start, end )
-            return [ (end-start, self.offs, bytes().ljust(pad_left) +
-                self.text[spos:epos] + bytes().ljust(pad_right)) ]
+            return [ (end-start, self.offs, b''.ljust(pad_left) +
+                self.text[spos:epos] + b''.ljust(pad_right)) ]
         elif self.end:
             # use text passed as parameter (text)
             spos, epos, pad_left, pad_right = calc_trim_text(
@@ -345,7 +351,7 @@ def line_width( segs ):
     """
     sc = 0
     seglist = segs
-    if segs and len(segs[0])==2 and segs[0][1]==None:
+    if segs and len(segs[0])==2 and segs[0][1] is None:
         seglist = segs[1:]
     for s in seglist:
         sc += s[0]
@@ -359,7 +365,7 @@ def shift_line( segs, amount ):
     """
     assert type(amount)==int, repr(amount)
 
-    if segs and len(segs[0])==2 and segs[0][1]==None:
+    if segs and len(segs[0])==2 and segs[0][1] is None:
         # existing shift
         amount += segs[0][0]
         if amount:
@@ -477,8 +483,8 @@ def calc_pos( text, layout, pref_col, row ):
     if pos is not None:
         return pos
 
-    rows_above = list(xrange(row-1,-1,-1))
-    rows_below = list(xrange(row+1,len(layout)))
+    rows_above = list(range(row-1,-1,-1))
+    rows_below = list(range(row+1,len(layout)))
     while rows_above and rows_below:
         if rows_above:
             r = rows_above.pop(0)
diff --git a/urwid/treetools.py b/urwid/treetools.py
index ea979c8af..769da495c 100644
--- a/urwid/treetools.py
+++ b/urwid/treetools.py
@@ -18,9 +18,8 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
 """
 Urwid tree view
@@ -31,6 +30,8 @@
 """
 
 
+from __future__ import annotations
+
 import urwid
 from urwid.wimp import SelectableIcon
 
@@ -51,7 +52,7 @@ def __init__(self, node):
         self.is_leaf = not hasattr(node, 'get_first_child')
         self.expanded = True
         widget = self.get_indented_widget()
-        self.__super.__init__(widget)
+        super().__init__(widget)
 
     def selectable(self):
         """
@@ -90,8 +91,7 @@ def get_node(self):
         return self._node
 
     def get_display_text(self):
-        return (self.get_node().get_key() + ": " +
-                str(self.get_node().get_value()))
+        return (f"{self.get_node().get_key()}: {str(self.get_node().get_value())}")
 
     def next_inorder(self):
         """Return the next TreeWidget depth first from this one."""
@@ -150,7 +150,7 @@ def keypress(self, size, key):
             self.expanded = False
             self.update_expanded_icon()
         elif self._w.selectable():
-            return self.__super.keypress(size, key)
+            return super().keypress(size, key)
         else:
             return key
 
@@ -193,7 +193,7 @@ def last_child(self):
                 return lastdescendant
 
 
-class TreeNode(object):
+class TreeNode:
     """
     Store tree contents and cache TreeWidget objects.
     A TreeNode consists of the following elements:
@@ -243,7 +243,7 @@ def change_key(self, key):
         self.get_parent().change_child_key(self._key, key)
 
     def get_parent(self):
-        if self._parent == None and self.get_depth() > 0:
+        if self._parent is None and self.get_depth() > 0:
             self._parent = self.load_parent()
         return self._parent
 
@@ -320,7 +320,7 @@ def set_child_node(self, key, node):
 
     def change_child_key(self, oldkey, newkey):
         if newkey in self._children:
-            raise TreeWidgetError("%s is already in use" % newkey)
+            raise TreeWidgetError(f"{newkey} is already in use")
         self._children[newkey] = self._children.pop(oldkey)
         self._children[newkey].set_key(newkey)
 
@@ -328,8 +328,7 @@ def get_child_index(self, key):
         try:
             return self.get_child_keys().index(key)
         except ValueError:
-            errorstring = ("Can't find key %s in ParentNode %s\n" +
-                           "ParentNode items: %s")
+            errorstring = f"Can't find key %s in ParentNode %s\nParentNode items: %s"
             raise TreeWidgetError(errorstring % (key, self.get_key(),
                                   str(self.get_child_keys())))
 
@@ -418,7 +417,7 @@ class TreeListBox(urwid.ListBox):
     collapsing of TreeWidgets"""
 
     def keypress(self, size, key):
-        key = self.__super.keypress(size, key)
+        key = super().keypress(size, key)
         return self.unhandled_input(size, key)
 
     def unhandled_input(self, size, input):
diff --git a/urwid/util.py b/urwid/util.py
index 6173ca29c..3ac465797 100644
--- a/urwid/util.py
+++ b/urwid/util.py
@@ -1,5 +1,4 @@
 #!/usr/bin/python
-# -*- coding: utf-8 -*-
 #
 # Urwid utility functions
 #    Copyright (C) 2004-2011  Ian Ward
@@ -18,15 +17,15 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
-from urwid import escape
-from urwid.compat import text_type, text_types
+from __future__ import annotations
 
 import codecs
 
+from urwid import escape
+
 str_util = escape.str_util
 
 # bring str_util functions into our namespace
@@ -98,7 +97,7 @@ def set_encoding( encoding ):
     _target_encoding = 'ascii'
     try:
         if encoding:
-            u"".encode(encoding)
+            "".encode(encoding)
             _target_encoding = encoding
     except LookupError: pass
 
@@ -116,12 +115,12 @@ def apply_target_encoding( s ):
     """
     Return (encoded byte string, character set rle).
     """
-    if _use_dec_special and type(s) == text_type:
+    if _use_dec_special and type(s) == str:
         # first convert drawing characters
         s = s.translate(escape.DEC_SPECIAL_CHARMAP)
 
-    if type(s) == text_type:
-        s = s.replace(escape.SI+escape.SO, u"") # remove redundant shifts
+    if type(s) == str:
+        s = s.replace(escape.SI+escape.SO, "") # remove redundant shifts
         s = codecs.encode(s, _target_encoding, 'replace')
 
     assert isinstance(s, bytes)
@@ -132,7 +131,7 @@ def apply_target_encoding( s ):
 
     assert isinstance(sis[0], bytes)
 
-    sis0 = sis[0].replace(SI, bytes())
+    sis0 = sis[0].replace(SI, b'')
     sout = []
     cout = []
     if sis0:
@@ -153,7 +152,7 @@ def apply_target_encoding( s ):
             rle_append_modify(cout, (escape.DEC_TAG.encode('ascii'), len(sin)))
             continue
         sin, son = sl
-        son = son.replace(SI, bytes())
+        son = son.replace(SI, b'')
         if sin:
             sout.append(sin)
             rle_append_modify(cout, (escape.DEC_TAG, len(sin)))
@@ -161,7 +160,7 @@ def apply_target_encoding( s ):
             sout.append(son)
             rle_append_modify(cout, (None, len(son)))
 
-    outstr = bytes().join(sout)
+    outstr = b''.join(sout)
     return outstr, cout
 
 
@@ -230,8 +229,8 @@ def trim_text_attr_cs( text, attr, cs, start_col, end_col ):
         rle_append_modify( attrtr, (al, 1) )
         rle_append_modify( cstr, (None, 1) )
 
-    return (bytes().rjust(pad_left) + text[spos:epos] +
-        bytes().rjust(pad_right), attrtr, cstr)
+    return (b''.rjust(pad_left) + text[spos:epos] +
+        b''.rjust(pad_right), attrtr, cstr)
 
 
 def rle_get_at( rle, pos ):
@@ -409,13 +408,13 @@ def _tagmarkup_recurse( tm, attr ):
     if type(tm) == tuple:
         # tuples mark a new attribute boundary
         if len(tm) != 2:
-            raise TagMarkupException("Tuples must be in the form (attribute, tagmarkup): %r" % (tm,))
+            raise TagMarkupException(f"Tuples must be in the form (attribute, tagmarkup): {tm!r}")
 
         attr, element = tm
         return _tagmarkup_recurse( element, attr )
 
-    if not isinstance(tm, text_types + (bytes,)):
-        raise TagMarkupException("Invalid markup element: %r" % tm)
+    if not isinstance(tm, (str, bytes)):
+        raise TagMarkupException(f"Invalid markup element: {tm!r}")
 
     # text
     return [tm], [(attr, len(tm))]
@@ -429,14 +428,13 @@ def is_mouse_press( ev ):
     return ev.find("press")>=0
 
 
-
 class MetaSuper(type):
     """adding .__super"""
     def __init__(cls, name, bases, d):
-        super(MetaSuper, cls).__init__(name, bases, d)
-        if hasattr(cls, "_%s__super" % name):
+        super().__init__(name, bases, d)
+        if hasattr(cls, f"_{name}__super"):
             raise AttributeError("Class has same name as one of its super classes")
-        setattr(cls, "_%s__super" % name, super(cls))
+        setattr(cls, f"_{name}__super", super(cls))
 
 
 
@@ -461,7 +459,7 @@ def int_scale(val, val_range, out_range):
     return num // dem
 
 
-class StoppingContext(object):
+class StoppingContext:
     """Context manager that calls ``stop`` on a given object on exit.  Used to
     make the ``start`` method on `MainLoop` and `BaseScreen` optionally act as
     context managers.
diff --git a/urwid/version.py b/urwid/version.py
index f88733b7a..46fbe7577 100644
--- a/urwid/version.py
+++ b/urwid/version.py
@@ -1,4 +1,2 @@
-from __future__ import division, print_function
-
 VERSION = (2, 1, 3, 'dev0')
 __version__ = '.'.join([str(x) for x in VERSION])
diff --git a/urwid/vterm.py b/urwid/vterm.py
index 761a602b7..aee0bae73 100644
--- a/urwid/vterm.py
+++ b/urwid/vterm.py
@@ -18,36 +18,36 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
-import os
-import sys
-import time
+from __future__ import annotations
+
+import atexit
 import copy
 import errno
+import os
 import select
-import struct
 import signal
-import atexit
+import struct
+import sys
+import time
 import traceback
 
 try:
-    import pty
     import fcntl
+    import pty
     import termios
 except ImportError:
     pass # windows
 
 from urwid import util
-from urwid.escape import DEC_SPECIAL_CHARS, ALT_DEC_SPECIAL_CHARS
 from urwid.canvas import Canvas
-from urwid.widget import Widget, BOX
-from urwid.display_common import AttrSpec, RealTerminal, _BASIC_COLORS
-from urwid.compat import ord2, chr2, B, PYTHON3, xrange
+from urwid.display_common import _BASIC_COLORS, AttrSpec, RealTerminal
+from urwid.escape import ALT_DEC_SPECIAL_CHARS, DEC_SPECIAL_CHARS
+from urwid.widget import BOX, Widget
 
-EOF = B('')
+EOF = b''
 ESC = chr(27)
 
 KEY_TRANSLATIONS = {
@@ -55,41 +55,41 @@
     'backspace': chr(127),
     'tab':       chr(9),
     'esc':       ESC,
-    'up':        ESC + '[A',
-    'down':      ESC + '[B',
-    'right':     ESC + '[C',
-    'left':      ESC + '[D',
-    'home':      ESC + '[1~',
-    'insert':    ESC + '[2~',
-    'delete':    ESC + '[3~',
-    'end':       ESC + '[4~',
-    'page up':   ESC + '[5~',
-    'page down': ESC + '[6~',
-
-    'f1':        ESC + '[[A',
-    'f2':        ESC + '[[B',
-    'f3':        ESC + '[[C',
-    'f4':        ESC + '[[D',
-    'f5':        ESC + '[[E',
-    'f6':        ESC + '[17~',
-    'f7':        ESC + '[18~',
-    'f8':        ESC + '[19~',
-    'f9':        ESC + '[20~',
-    'f10':       ESC + '[21~',
-    'f11':       ESC + '[23~',
-    'f12':       ESC + '[24~',
+    'up':        f"{ESC}[A",
+    'down':      f"{ESC}[B",
+    'right':     f"{ESC}[C",
+    'left':      f"{ESC}[D",
+    'home':      f"{ESC}[1~",
+    'insert':    f"{ESC}[2~",
+    'delete':    f"{ESC}[3~",
+    'end':       f"{ESC}[4~",
+    'page up':   f"{ESC}[5~",
+    'page down': f"{ESC}[6~",
+
+    'f1':        f"{ESC}[[A",
+    'f2':        f"{ESC}[[B",
+    'f3':        f"{ESC}[[C",
+    'f4':        f"{ESC}[[D",
+    'f5':        f"{ESC}[[E",
+    'f6':        f"{ESC}[17~",
+    'f7':        f"{ESC}[18~",
+    'f8':        f"{ESC}[19~",
+    'f9':        f"{ESC}[20~",
+    'f10':       f"{ESC}[21~",
+    'f11':       f"{ESC}[23~",
+    'f12':       f"{ESC}[24~",
 }
 
 KEY_TRANSLATIONS_DECCKM = {
-    'up':        ESC + 'OA',
-    'down':      ESC + 'OB',
-    'right':     ESC + 'OC',
-    'left':      ESC + 'OD',
-    'f1':        ESC + 'OP',
-    'f2':        ESC + 'OQ',
-    'f3':        ESC + 'OR',
-    'f4':        ESC + 'OS',
-    'f5':        ESC + '[15~',
+    'up':        f"{ESC}OA",
+    'down':      f"{ESC}OB",
+    'right':     f"{ESC}OC",
+    'left':      f"{ESC}OD",
+    'f1':        f"{ESC}OP",
+    'f2':        f"{ESC}OQ",
+    'f3':        f"{ESC}OR",
+    'f4':        f"{ESC}OS",
+    'f5':        f"{ESC}[15~",
 }
 
 CSI_COMMANDS = {
@@ -101,44 +101,44 @@
     # while callback is executed as:
     #     callback(<instance of TermCanvas>, arguments, has_question_mark)
 
-    B('@'): (1, 1, lambda s, number, q: s.insert_chars(chars=number[0])),
-    B('A'): (1, 1, lambda s, rows, q: s.move_cursor(0, -rows[0], relative=True)),
-    B('B'): (1, 1, lambda s, rows, q: s.move_cursor(0, rows[0], relative=True)),
-    B('C'): (1, 1, lambda s, cols, q: s.move_cursor(cols[0], 0, relative=True)),
-    B('D'): (1, 1, lambda s, cols, q: s.move_cursor(-cols[0], 0, relative=True)),
-    B('E'): (1, 1, lambda s, rows, q: s.move_cursor(0, rows[0], relative_y=True)),
-    B('F'): (1, 1, lambda s, rows, q: s.move_cursor(0, -rows[0], relative_y=True)),
-    B('G'): (1, 1, lambda s, col, q: s.move_cursor(col[0] - 1, 0, relative_y=True)),
-    B('H'): (2, 1, lambda s, x_y, q: s.move_cursor(x_y[1] - 1, x_y[0] - 1)),
-    B('J'): (1, 0, lambda s, mode, q: s.csi_erase_display(mode[0])),
-    B('K'): (1, 0, lambda s, mode, q: s.csi_erase_line(mode[0])),
-    B('L'): (1, 1, lambda s, number, q: s.insert_lines(lines=number[0])),
-    B('M'): (1, 1, lambda s, number, q: s.remove_lines(lines=number[0])),
-    B('P'): (1, 1, lambda s, number, q: s.remove_chars(chars=number[0])),
-    B('X'): (1, 1, lambda s, number, q: s.erase(s.term_cursor,
+    b'@': (1, 1, lambda s, number, q: s.insert_chars(chars=number[0])),
+    b'A': (1, 1, lambda s, rows, q: s.move_cursor(0, -rows[0], relative=True)),
+    b'B': (1, 1, lambda s, rows, q: s.move_cursor(0, rows[0], relative=True)),
+    b'C': (1, 1, lambda s, cols, q: s.move_cursor(cols[0], 0, relative=True)),
+    b'D': (1, 1, lambda s, cols, q: s.move_cursor(-cols[0], 0, relative=True)),
+    b'E': (1, 1, lambda s, rows, q: s.move_cursor(0, rows[0], relative_y=True)),
+    b'F': (1, 1, lambda s, rows, q: s.move_cursor(0, -rows[0], relative_y=True)),
+    b'G': (1, 1, lambda s, col, q: s.move_cursor(col[0] - 1, 0, relative_y=True)),
+    b'H': (2, 1, lambda s, x_y, q: s.move_cursor(x_y[1] - 1, x_y[0] - 1)),
+    b'J': (1, 0, lambda s, mode, q: s.csi_erase_display(mode[0])),
+    b'K': (1, 0, lambda s, mode, q: s.csi_erase_line(mode[0])),
+    b'L': (1, 1, lambda s, number, q: s.insert_lines(lines=number[0])),
+    b'M': (1, 1, lambda s, number, q: s.remove_lines(lines=number[0])),
+    b'P': (1, 1, lambda s, number, q: s.remove_chars(chars=number[0])),
+    b'X': (1, 1, lambda s, number, q: s.erase(s.term_cursor,
                                                 (s.term_cursor[0]+number[0] - 1,
                                                  s.term_cursor[1]))),
-    B('a'): ('alias', B('C')),
-    B('c'): (0, 0, lambda s, none, q: s.csi_get_device_attributes(q)),
-    B('d'): (1, 1, lambda s, row, q: s.move_cursor(0, row[0] - 1, relative_x=True)),
-    B('e'): ('alias', B('B')),
-    B('f'): ('alias', B('H')),
-    B('g'): (1, 0, lambda s, mode, q: s.csi_clear_tabstop(mode[0])),
-    B('h'): (1, 0, lambda s, modes, q: s.csi_set_modes(modes, q)),
-    B('l'): (1, 0, lambda s, modes, q: s.csi_set_modes(modes, q, reset=True)),
-    B('m'): (1, 0, lambda s, attrs, q: s.csi_set_attr(attrs)),
-    B('n'): (1, 0, lambda s, mode, q: s.csi_status_report(mode[0])),
-    B('q'): (1, 0, lambda s, mode, q: s.csi_set_keyboard_leds(mode[0])),
-    B('r'): (2, 0, lambda s, t_b, q: s.csi_set_scroll(t_b[0], t_b[1])),
-    B('s'): (0, 0, lambda s, none, q: s.save_cursor()),
-    B('u'): (0, 0, lambda s, none, q: s.restore_cursor()),
-    B('`'): ('alias', B('G')),
+    b'a': ('alias', b'C'),
+    b'c': (0, 0, lambda s, none, q: s.csi_get_device_attributes(q)),
+    b'd': (1, 1, lambda s, row, q: s.move_cursor(0, row[0] - 1, relative_x=True)),
+    b'e': ('alias', b'B'),
+    b'f': ('alias', b'H'),
+    b'g': (1, 0, lambda s, mode, q: s.csi_clear_tabstop(mode[0])),
+    b'h': (1, 0, lambda s, modes, q: s.csi_set_modes(modes, q)),
+    b'l': (1, 0, lambda s, modes, q: s.csi_set_modes(modes, q, reset=True)),
+    b'm': (1, 0, lambda s, attrs, q: s.csi_set_attr(attrs)),
+    b'n': (1, 0, lambda s, mode, q: s.csi_status_report(mode[0])),
+    b'q': (1, 0, lambda s, mode, q: s.csi_set_keyboard_leds(mode[0])),
+    b'r': (2, 0, lambda s, t_b, q: s.csi_set_scroll(t_b[0], t_b[1])),
+    b's': (0, 0, lambda s, none, q: s.save_cursor()),
+    b'u': (0, 0, lambda s, none, q: s.restore_cursor()),
+    b'`': ('alias', b'G'),
 }
 
 CHARSET_DEFAULT = 1
 CHARSET_UTF8 = 2
 
-class TermModes(object):
+class TermModes:
     def __init__(self):
         self.reset()
 
@@ -158,7 +158,7 @@ def reset(self):
         # charset stuff
         self.main_charset = CHARSET_DEFAULT
 
-class TermCharset(object):
+class TermCharset:
     MAPPING = {
         'default': None,
         'vt100':   '0',
@@ -228,15 +228,15 @@ def trunc(self):
 
     def append(self, obj):
         self.trunc()
-        super(TermScroller, self).append(obj)
+        super().append(obj)
 
     def insert(self, idx, obj):
         self.trunc()
-        super(TermScroller, self).insert(idx, obj)
+        super().insert(idx, obj)
 
     def extend(self, seq):
         self.trunc()
-        super(TermScroller, self).extend(seq)
+        super().extend(seq)
 
 class TermCanvas(Canvas):
     cacheable = False
@@ -253,7 +253,7 @@ def __init__(self, width, height, widget):
         self.scrolling_up = 0
 
         self.utf8_eat_bytes = None
-        self.utf8_buffer = bytes()
+        self.utf8_buffer = b''
 
         self.coords["cursor"] = (0, 0, None)
 
@@ -318,7 +318,7 @@ def reset(self):
         """
         Reset the terminal.
         """
-        self.escbuf = bytes()
+        self.escbuf = b''
         self.within_escape = False
         self.parsestate = 0
 
@@ -353,7 +353,7 @@ def init_tabstops(self, extend=False):
 
     def set_tabstop(self, x=None, remove=False, clear=False):
         if clear:
-            for tab in xrange(len(self.tabstops)):
+            for tab in range(len(self.tabstops)):
                 self.tabstops[tab] = 0
             return
 
@@ -373,10 +373,10 @@ def is_tabstop(self, x=None):
         div, mod = divmod(x, 8)
         return (self.tabstops[div] & (1 << mod)) > 0
 
-    def empty_line(self, char=B(' ')):
+    def empty_line(self, char=b' '):
         return [self.empty_char(char)] * self.width
 
-    def empty_char(self, char=B(' ')):
+    def empty_char(self, char=b' '):
         return (self.attrspec, self.charset.current, char)
 
     def addstr(self, data):
@@ -385,7 +385,7 @@ def addstr(self, data):
             return
 
         for byte in data:
-            self.addbyte(ord2(byte))
+            self.addbyte(byte)
 
     def resize(self, width, height):
         """
@@ -395,18 +395,18 @@ def resize(self, width, height):
 
         if width > self.width:
             # grow
-            for y in xrange(self.height):
+            for y in range(self.height):
                 self.term[y] += [self.empty_char()] * (width - self.width)
         elif width < self.width:
             # shrink
-            for y in xrange(self.height):
+            for y in range(self.height):
                 self.term[y] = self.term[y][:width]
 
         self.width = width
 
         if height > self.height:
             # grow
-            for y in xrange(self.height, height):
+            for y in range(self.height, height):
                 try:
                     last_line = self.scrollback_buffer.pop()
                 except IndexError:
@@ -427,7 +427,7 @@ def resize(self, width, height):
                 self.term.insert(0, last_line)
         elif height < self.height:
             # shrink
-            for y in xrange(height, self.height):
+            for y in range(height, self.height):
                 self.scrollback_buffer.append(self.term.pop(0))
 
         self.height = height
@@ -447,16 +447,16 @@ def set_g01(self, char, mod):
         if self.modes.main_charset != CHARSET_DEFAULT:
             return
 
-        if mod == B('('):
+        if mod == b'(':
             g = 0
         else:
             g = 1
 
-        if char == B('0'):
+        if char == b'0':
             cset = 'vt100'
-        elif char == B('U'):
+        elif char == b'U':
             cset = 'ibmpc'
-        elif char == B('K'):
+        elif char == b'K':
             cset = 'user'
         else:
             cset = 'default'
@@ -467,10 +467,10 @@ def parse_csi(self, char):
         """
         Parse ECMA-48 CSI (Control Sequence Introducer) sequences.
         """
-        qmark = self.escbuf.startswith(B('?'))
+        qmark = self.escbuf.startswith(b'?')
 
         escbuf = []
-        for arg in self.escbuf[qmark and 1 or 0:].split(B(';')):
+        for arg in self.escbuf[qmark and 1 or 0:].split(b';'):
             try:
                 num = int(arg)
             except ValueError:
@@ -487,7 +487,7 @@ def parse_csi(self, char):
             number_of_args, default_value, cmd = csi_cmd
             while len(escbuf) < number_of_args:
                 escbuf.append(default_value)
-            for i in xrange(len(escbuf)):
+            for i in range(len(escbuf)):
                 if escbuf[i] is None or escbuf[i] == 0:
                     escbuf[i] = default_value
 
@@ -502,40 +502,40 @@ def parse_noncsi(self, char, mod=None):
         """
         Parse escape sequences which are not CSI.
         """
-        if mod == B('#') and char == B('8'):
+        if mod == b'#' and char == b'8':
             self.decaln()
-        elif mod == B('%'): # select main character set
-            if char == B('@'):
+        elif mod == b'%': # select main character set
+            if char == b'@':
                 self.modes.main_charset = CHARSET_DEFAULT
-            elif char in B('G8'):
+            elif char in b'G8':
                 # 8 is obsolete and only for backwards compatibility
                 self.modes.main_charset = CHARSET_UTF8
-        elif mod == B('(') or mod == B(')'): # define G0/G1
+        elif mod == b'(' or mod == b')': # define G0/G1
             self.set_g01(char, mod)
-        elif char == B('M'): # reverse line feed
+        elif char == b'M': # reverse line feed
             self.linefeed(reverse=True)
-        elif char == B('D'): # line feed
+        elif char == b'D': # line feed
             self.linefeed()
-        elif char == B('c'): # reset terminal
+        elif char == b'c': # reset terminal
             self.reset()
-        elif char == B('E'): # newline
+        elif char == b'E': # newline
             self.newline()
-        elif char == B('H'): # set tabstop
+        elif char == b'H': # set tabstop
             self.set_tabstop()
-        elif char == B('Z'): # DECID
-            self.widget.respond(ESC + '[?6c')
-        elif char == B('7'): # save current state
+        elif char == b'Z': # DECID
+            self.widget.respond(f"{ESC}[?6c")
+        elif char == b'7': # save current state
             self.save_cursor(with_attrs=True)
-        elif char == B('8'): # restore current state
+        elif char == b'8': # restore current state
             self.restore_cursor(with_attrs=True)
 
     def parse_osc(self, buf):
         """
         Parse operating system command.
         """
-        if buf.startswith(B(';')): # set window title and icon
+        if buf.startswith(b';'): # set window title and icon
             self.widget.set_title(buf[1:])
-        elif buf.startswith(B('3;')): # set window title
+        elif buf.startswith(b'3;'): # set window title
             self.widget.set_title(buf[2:])
 
     def parse_escape(self, char):
@@ -544,43 +544,43 @@ def parse_escape(self, char):
             if char in CSI_COMMANDS.keys():
                 self.parse_csi(char)
                 self.parsestate = 0
-            elif char in B('0123456789;') or (not self.escbuf and char == B('?')):
+            elif char in b'0123456789;' or (not self.escbuf and char == b'?'):
                 self.escbuf += char
                 return
-        elif self.parsestate == 0 and char == B(']'):
+        elif self.parsestate == 0 and char == b']':
             # start of OSC
-            self.escbuf = bytes()
+            self.escbuf = b''
             self.parsestate = 2
             return
-        elif self.parsestate == 2 and char == B("\x07"):
+        elif self.parsestate == 2 and char == b"\x07":
             # end of OSC
-            self.parse_osc(self.escbuf.lstrip(B('0')))
-        elif self.parsestate == 2 and self.escbuf[-1:] + char == B(ESC + '\\'):
+            self.parse_osc(self.escbuf.lstrip(b'0'))
+        elif self.parsestate == 2 and self.escbuf[-1:] + char == f"{ESC}\\".encode('iso8859-1'):
             # end of OSC
-            self.parse_osc(self.escbuf[:-1].lstrip(B('0')))
-        elif self.parsestate == 2 and self.escbuf.startswith(B('P')) and \
+            self.parse_osc(self.escbuf[:-1].lstrip(b'0'))
+        elif self.parsestate == 2 and self.escbuf.startswith(b'P') and \
              len(self.escbuf) == 8:
             # set palette (ESC]Pnrrggbb)
             pass
-        elif self.parsestate == 2 and not self.escbuf and char == B('R'):
+        elif self.parsestate == 2 and not self.escbuf and char == b'R':
             # reset palette
             pass
         elif self.parsestate == 2:
             self.escbuf += char
             return
-        elif self.parsestate == 0 and char == B('['):
+        elif self.parsestate == 0 and char == b'[':
             # start of CSI
-            self.escbuf = bytes()
+            self.escbuf = b''
             self.parsestate = 1
             return
-        elif self.parsestate == 0 and char in (B('%'), B('#'), B('('), B(')')):
+        elif self.parsestate == 0 and char in (b'%', b'#', b'(', b')'):
             # non-CSI sequence
             self.escbuf = char
             self.parsestate = 3
             return
         elif self.parsestate == 3:
             self.parse_noncsi(char, self.escbuf)
-        elif char in (B('c'), B('D'), B('E'), B('H'), B('M'), B('Z'), B('7'), B('8'), B('>'), B('=')):
+        elif char in (b'c', b'D', b'E', b'H', b'M', b'Z', b'7', b'8', b'>', b'='):
             self.parse_noncsi(char)
 
         self.leave_escape()
@@ -588,7 +588,7 @@ def parse_escape(self, char):
     def leave_escape(self):
         self.within_escape = False
         self.parsestate = 0
-        self.escbuf = bytes()
+        self.escbuf = b''
 
     def get_utf8_len(self, bytenum):
         """
@@ -617,27 +617,27 @@ def addbyte(self, byte):
             if byte >= 0xc0:
                 # start multibyte sequence
                 self.utf8_eat_bytes = self.get_utf8_len(byte)
-                self.utf8_buffer = chr2(byte)
+                self.utf8_buffer = bytes([byte])
                 return
             elif 0x80 <= byte < 0xc0 and self.utf8_eat_bytes is not None:
                 if self.utf8_eat_bytes > 1:
                     # continue multibyte sequence
                     self.utf8_eat_bytes -= 1
-                    self.utf8_buffer += chr2(byte)
+                    self.utf8_buffer += bytes([byte])
                     return
                 else:
                     # end multibyte sequence
                     self.utf8_eat_bytes = None
-                    sequence = (self.utf8_buffer+chr2(byte)).decode('utf-8', 'ignore')
+                    sequence = (self.utf8_buffer+bytes([byte])).decode('utf-8', 'ignore')
                     if len(sequence) == 0:
                         # invalid multibyte sequence, stop processing
                         return
                     char = sequence.encode(util._target_encoding, 'replace')
             else:
                 self.utf8_eat_bytes = None
-                char = chr2(byte)
+                char = bytes([byte])
         else:
-            char = chr2(byte)
+            char = bytes([byte])
 
         self.process_char(char)
 
@@ -654,36 +654,36 @@ def process_char(self, char):
 
         dc = self.modes.display_ctrl
 
-        if char == B("\x1b") and self.parsestate != 2: # escape
+        if char == b"\x1b" and self.parsestate != 2: # escape
             self.within_escape = True
-        elif not dc and char == B("\x0d"): # carriage return
+        elif not dc and char == b"\x0d": # carriage return
             self.carriage_return()
-        elif not dc and char == B("\x0f"): # activate G0
+        elif not dc and char == b"\x0f": # activate G0
             self.charset.activate(0)
-        elif not dc and char == B("\x0e"): # activate G1
+        elif not dc and char == b"\x0e": # activate G1
             self.charset.activate(1)
-        elif not dc and char in B("\x0a\x0b\x0c"): # line feed
+        elif not dc and char in b"\x0a\x0b\x0c": # line feed
             self.linefeed()
             if self.modes.lfnl:
                 self.carriage_return()
-        elif not dc and char == B("\x09"): # char tab
+        elif not dc and char == b"\x09": # char tab
             self.tab()
-        elif not dc and char == B("\x08"): # backspace
+        elif not dc and char == b"\x08": # backspace
             if x > 0:
                 self.set_term_cursor(x - 1, y)
-        elif not dc and char == B("\x07") and self.parsestate != 2: # beep
+        elif not dc and char == b"\x07" and self.parsestate != 2: # beep
             # we need to check if we're in parsestate 2, as an OSC can be
             # terminated by the BEL character!
             self.widget.beep()
-        elif not dc and char in B("\x18\x1a"): # CAN/SUB
+        elif not dc and char in b"\x18\x1a": # CAN/SUB
             self.leave_escape()
-        elif not dc and char in B("\x00\x7f"): # NUL/DEL
+        elif not dc and char in b"\x00\x7f": # NUL/DEL
             pass # this is ignored
         elif self.within_escape:
             self.parse_escape(char)
-        elif not dc and char == B("\x9b"): # CSI (equivalent to "ESC [")
+        elif not dc and char == b"\x9b": # CSI (equivalent to "ESC [")
             self.within_escape = True
-            self.escbuf = bytes()
+            self.escbuf = b''
             self.parsestate = 1
         else:
             self.push_cursor(char)
@@ -855,7 +855,7 @@ def tab(self, tabstop=8):
         x, y = self.term_cursor
 
         while x < self.width - 1:
-            self.set_char(B(" "))
+            self.set_char(b" ")
             x += 1
 
             if self.is_tabstop(x):
@@ -884,7 +884,7 @@ def decaln(self):
         """
         DEC screen alignment test: Fill screen with E's.
         """
-        for row in xrange(self.height):
+        for row in range(self.height):
             self.term[row] = self.empty_line('E')
 
     def blank_line(self, row):
@@ -991,7 +991,7 @@ def erase(self, start, end):
 
         # within a single row
         if sy == ey:
-            for x in xrange(sx, ex + 1):
+            for x in range(sx, ex + 1):
                 self.term[sy][x] = self.empty_char()
             return
 
@@ -999,10 +999,10 @@ def erase(self, start, end):
         y = sy
         while y <= ey:
             if y == sy:
-                for x in xrange(sx, self.width):
+                for x in range(sx, self.width):
                     self.term[y][x] = self.empty_char()
             elif y == ey:
-                for x in xrange(ex + 1):
+                for x in range(ex + 1):
                     self.term[y][x] = self.empty_char()
             else:
                 self.blank_line(y)
@@ -1137,8 +1137,8 @@ def reverse_video(self, undo=False):
         """
         Reverse video/scanmode (DECSCNM) by swapping fg and bg colors.
         """
-        for y in xrange(self.height):
-            for x in xrange(self.width):
+        for y in range(self.height):
+            for x in range(self.width):
                 char = self.term[y][x]
                 attrs = self.reverse_attrspec(char[0], undo=undo)
                 self.term[y][x] = (attrs,) + char[1:]
@@ -1222,7 +1222,7 @@ def csi_get_device_attributes(self, qmark):
         ourself as a VT102 terminal.
         """
         if not qmark:
-            self.widget.respond(ESC + '[?6c')
+            self.widget.respond(f"{ESC}[?6c")
 
     def csi_status_report(self, mode):
         """
@@ -1233,7 +1233,7 @@ def csi_status_report(self, mode):
         """
         if mode == 5:
             # terminal OK
-            self.widget.respond(ESC + '[0n')
+            self.widget.respond(f"{ESC}[0n")
         elif mode == 6:
             x, y = self.term_cursor
             self.widget.respond(ESC + '[%d;%dR' % (y + 1, x + 1))
@@ -1294,7 +1294,7 @@ def clear(self, cursor=None):
         Clears the whole terminal screen and resets the cursor position
         to (0, 0) or to the coordinates given by 'cursor'.
         """
-        self.term = [self.empty_line() for x in xrange(self.height)]
+        self.term = [self.empty_line() for x in range(self.height)]
 
         if cursor is None:
             self.set_term_cursor(0, 0)
@@ -1310,12 +1310,10 @@ def rows(self):
     def content(self, trim_left=0, trim_right=0, cols=None, rows=None,
                 attr_map=None):
         if self.scrolling_up == 0:
-            for line in self.term:
-                yield line
+            yield from self.term
         else:
             buf = self.scrollback_buffer + self.term
-            for line in buf[-(self.height+self.scrolling_up):-self.scrolling_up]:
-                yield line
+            yield from buf[-(self.height+self.scrolling_up):-self.scrolling_up]
 
     def content_delta(self, other):
         if other is self:
@@ -1558,7 +1556,7 @@ def wait_and_feed(self, timeout=1.0):
             try:
                 select.select([self.master], [], [], timeout)
                 break
-            except select.error as e:
+            except OSError as e:
                 if e.args[0] != 4:
                     raise
         self.feed()
@@ -1656,7 +1654,6 @@ def keypress(self, size, key):
         if self.term_modes.lfnl and key == "\x0d":
             key += "\x0a"
 
-        if PYTHON3:
-            key = key.encode(self.encoding, 'ignore')
+        key = key.encode(self.encoding, 'ignore')
 
         os.write(self.master, key)
diff --git a/urwid/web_display.py b/urwid/web_display.py
index 2b2de46a6..37e021a55 100755
--- a/urwid/web_display.py
+++ b/urwid/web_display.py
@@ -17,22 +17,24 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
 """
 Urwid web application display module
 """
+from __future__ import annotations
+
+import glob
 import os
-import sys
-import signal
 import random
 import select
+import signal
 import socket
-import glob
+import sys
 
 from urwid import util
+
 _js_code = r"""
 // Urwid web (CGI/Asynchronous Javascript) display module
 //    Copyright (C) 2004-2005  Ian Ward
@@ -51,7 +53,7 @@
 //    License along with this library; if not, write to the Free Software
 //    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 //
-// Urwid web site: http://excess.org/urwid/
+// Urwid web site: https://urwid.org/
 
 colours = new Object();
 colours = {
@@ -551,7 +553,7 @@
 <head>
 <title>Urwid Web Display - ""","""</title>
 <style type="text/css">
-""" + _css_style + """
+""" + _css_style + r"""
 </style>
 </head>
 <body id="body" onload="load_web_display()">
@@ -578,7 +580,9 @@ def __init__(self):
         self.has_color = True
         self._started = False
 
-    started = property(lambda self: self._started)
+    @property
+    def started(self):
+        return self._started
 
     def register_palette( self, l ):
         """Register a list of palette entries.
@@ -596,7 +600,7 @@ def register_palette( self, l ):
             assert len(item) == 2, "Invalid register_palette usage"
             name, like_name = item
             if like_name not in self.palette:
-                raise Exception("palette entry '%s' doesn't exist"%like_name)
+                raise Exception(f"palette entry '{like_name}' doesn't exist")
             self.palette[name] = self.palette[like_name]
 
     def register_palette_entry( self, name, foreground, background,
@@ -660,11 +664,11 @@ def start(self):
 
         urwid_id = "%09d%09d"%(random.randrange(10**9),
             random.randrange(10**9))
-        self.pipe_name = os.path.join(_prefs.pipe_dir,"urwid"+urwid_id)
-        os.mkfifo(self.pipe_name+".in",0o600)
+        self.pipe_name = os.path.join(_prefs.pipe_dir,f"urwid{urwid_id}")
+        os.mkfifo(f"{self.pipe_name}.in",0o600)
         signal.signal(signal.SIGTERM,self._cleanup_pipe)
 
-        self.input_fd = os.open(self.pipe_name+".in",
+        self.input_fd = os.open(f"{self.pipe_name}.in",
             os.O_NONBLOCK | os.O_RDONLY)
         self.input_tail = ""
         self.content_head = ("Content-type: "
@@ -674,9 +678,7 @@ def start(self):
             "--ZZ\r\n")
         if self.update_method=="polling":
             self.content_head = (
-                "Content-type: text/plain\r\n"
-                "X-Urwid-ID: "+urwid_id+"\r\n"
-                "\r\n\r\n")
+                f'Content-type: text/plain\r\nX-Urwid-ID: {urwid_id}\r\n\r\n\r\n')
 
         signal.signal(signal.SIGALRM,self._handle_alarm)
         signal.alarm( ALARM_DELAY )
@@ -731,8 +733,8 @@ def _cleanup_pipe(self, *args):
         if not self.pipe_name: return
         # XXX which exceptions does this actually raise? EnvironmentError?
         try:
-            os.remove(self.pipe_name+".in")
-            os.remove(self.pipe_name+".update")
+            os.remove(f"{self.pipe_name}.in")
+            os.remove(f"{self.pipe_name}.update")
         except Exception:
             pass
 
@@ -817,7 +819,7 @@ def draw_screen(self, size, r ):
                 else:
                     l.append(code_span(run, fg, bg))
 
-            send("".join(l)+"\n")
+            send(f"{''.join(l)}\n")
         self.last_screen = new_screen
         self.last_screen_width = cols
 
@@ -851,12 +853,12 @@ def _fork_child(self):
         Fork a child to run CGI disconnected for polling update method.
         Force parent process to exit.
         """
-        daemonize( self.pipe_name +".err" )
-        self.input_fd = os.open(self.pipe_name+".in",
+        daemonize( f"{self.pipe_name}.err" )
+        self.input_fd = os.open(f"{self.pipe_name}.in",
             os.O_NONBLOCK | os.O_RDONLY)
         self.update_method = "polling child"
         s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-        s.bind( self.pipe_name+".update" )
+        s.bind( f"{self.pipe_name}.update" )
         s.listen(1)
         s.settimeout(POLL_CONNECT)
         self.server_socket = s
@@ -889,7 +891,7 @@ def get_input(self, raw_keys=False):
         try:
             iready,oready,eready = select.select(
                 [self.input_fd],[],[],0.5)
-        except select.error as e:
+        except OSError as e:
             # return on interruptions
             if e.args[0] == 4:
                 if raw_keys:
@@ -904,7 +906,7 @@ def get_input(self, raw_keys=False):
 
         keydata = os.read(self.input_fd, MAX_READ)
         os.close(self.input_fd)
-        self.input_fd = os.open(self.pipe_name+".in",
+        self.input_fd = os.open(f"{self.pipe_name}.in",
             os.O_NONBLOCK | os.O_RDONLY)
         #sys.stderr.write( repr((keydata,self.input_tail))+"\n" )
         keys = keydata.split("\n")
@@ -940,7 +942,7 @@ def code_span( s, fg, bg, cursor = -1):
              code_bg + code_fg + s[c_off:c2_off] + "\n" +
              code_fg + code_bg + s[c2_off:] + "\n")
     else:
-        return code_fg + code_bg + s + "\n"
+        return f"{code_fg + code_bg + s}\n"
 
 
 def html_escape(text):
@@ -1004,20 +1006,20 @@ def handle_short_request():
         s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
         try:
             s.connect( os.path.join(_prefs.pipe_dir,
-                "urwid"+urwid_id+".update") )
-            data = "Content-type: text/plain\r\n\r\n"+s.recv(BUF_SZ)
+                f"urwid{urwid_id}.update") )
+            data = f'Content-type: text/plain\r\n\r\n{s.recv(BUF_SZ)}'
             while data:
                 sys.stdout.write(data)
                 data = s.recv(BUF_SZ)
             return True
-        except socket.error:
+        except OSError:
             sys.stdout.write("Status: 404 Not Found\r\n\r\n")
             return True
 
     # this is a keyboard input request
     try:
         fd = os.open((os.path.join(_prefs.pipe_dir,
-            "urwid"+urwid_id+".in")), os.O_WRONLY)
+            f"urwid{urwid_id}.in")), os.O_WRONLY)
     except OSError:
         sys.stdout.write("Status: 404 Not Found\r\n\r\n")
         return True
@@ -1090,7 +1092,7 @@ def daemonize( errfile ):
         except OSError:
             pass
 
-    sys.stdin = open("/dev/null","r")
+    sys.stdin = open("/dev/null")
     sys.stdout = open("/dev/null","w")
     sys.stderr = ErrorLog( errfile )
 
diff --git a/urwid/widget.py b/urwid/widget.py
index b908d9f78..d0ae1fb5f 100644
--- a/urwid/widget.py
+++ b/urwid/widget.py
@@ -17,24 +17,36 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
-from operator import attrgetter
+from __future__ import annotations
 
-from urwid.compat import text_type, with_metaclass
-from urwid.util import (MetaSuper, decompose_tagmarkup, calc_width,
-    is_wide_char, move_prev_char, move_next_char)
-from urwid.text_layout import calc_pos, calc_coords, shift_line
-from urwid import signals
-from urwid import text_layout
-from urwid.canvas import (CanvasCache, CompositeCanvas, SolidCanvas,
-    apply_text_layout)
-from urwid.command_map import (command_map, CURSOR_LEFT, CURSOR_RIGHT,
-    CURSOR_UP, CURSOR_DOWN, CURSOR_MAX_LEFT, CURSOR_MAX_RIGHT)
-from urwid.split_repr import split_repr, remove_defaults, python3_repr
+import functools
+import warnings
+from operator import attrgetter
 
+from urwid import signals, text_layout
+from urwid.canvas import CanvasCache, CompositeCanvas, SolidCanvas, apply_text_layout
+from urwid.command_map import (
+    CURSOR_DOWN,
+    CURSOR_LEFT,
+    CURSOR_MAX_LEFT,
+    CURSOR_MAX_RIGHT,
+    CURSOR_RIGHT,
+    CURSOR_UP,
+    command_map,
+)
+from urwid.split_repr import remove_defaults, split_repr
+from urwid.text_layout import calc_coords, calc_pos, shift_line
+from urwid.util import (
+    MetaSuper,
+    calc_width,
+    decompose_tagmarkup,
+    is_wide_char,
+    move_next_char,
+    move_prev_char,
+)
 
 # define some names for these constants to avoid misspellings in the source
 # and to document the constant strings we are using
@@ -86,7 +98,7 @@ class WidgetMeta(MetaSuper, signals.MetaSignals):
     def __init__(cls, name, bases, d):
         no_cache = d.get("no_cache", [])
 
-        super(WidgetMeta, cls).__init__(name, bases, d)
+        super().__init__(name, bases, d)
 
         if "render" in d:
             if "render" not in no_cache:
@@ -105,28 +117,17 @@ def __init__(cls, name, bases, d):
 class WidgetError(Exception):
     pass
 
+
 def validate_size(widget, size, canv):
     """
     Raise a WidgetError if a canv does not match size size.
     """
     if (size and size[1:] != (0,) and size[0] != canv.cols()) or \
         (len(size)>1 and size[1] != canv.rows()):
-        raise WidgetError("Widget %r rendered (%d x %d) canvas"
-            " when passed size %r!" % (widget, canv.cols(),
-            canv.rows(), size))
-
-def update_wrapper(new_fn, fn):
-    """
-    Copy as much of the function detail from fn to new_fn
-    as we can.
-    """
-    try:
-        new_fn.__name__ = fn.__name__
-        new_fn.__dict__.update(fn.__dict__)
-        new_fn.__doc__ = fn.__doc__
-        new_fn.__module__ = fn.__module__
-    except TypeError:
-        pass # python2.3 ignore read-only attributes
+        raise WidgetError(
+            f"Widget {widget!r} rendered ({canv.cols():d} x {canv.rows():d}) canvas "
+            f"when passed size {size!r}!"
+        )
 
 
 def cache_widget_render(cls):
@@ -136,6 +137,8 @@ def cache_widget_render(cls):
     """
     ignore_focus = bool(getattr(cls, "ignore_focus", False))
     fn = cls.render
+
+    @functools.wraps(fn)
     def cached_render(self, size, focus=False):
         focus = focus and not ignore_focus
         canv = CanvasCache.fetch(self, cls, size, focus)
@@ -150,9 +153,9 @@ def cached_render(self, size, focus=False):
         CanvasCache.store(cls, canv)
         return canv
     cached_render.original_fn = fn
-    update_wrapper(cached_render, fn)
     return cached_render
 
+
 def nocache_widget_render(cls):
     """
     Return a function that wraps the cls.render() method
@@ -161,6 +164,8 @@ def nocache_widget_render(cls):
     fn = cls.render
     if hasattr(fn, "original_fn"):
         fn = fn.original_fn
+
+    @functools.wraps(fn)
     def finalize_render(self, size, focus=False):
         canv = fn(self, size, focus=focus)
         if canv.widget_info:
@@ -169,9 +174,9 @@ def finalize_render(self, size, focus=False):
         canv.finalize(self, size, focus)
         return canv
     finalize_render.original_fn = fn
-    update_wrapper(finalize_render, fn)
     return finalize_render
 
+
 def nocache_widget_render_instance(self):
     """
     Return a function that wraps the cls.render() method
@@ -179,6 +184,8 @@ def nocache_widget_render_instance(self):
     cache the canvas.
     """
     fn = self.render.original_fn
+
+    @functools.wraps(fn)
     def finalize_render(size, focus=False):
         canv = fn(self, size, focus=focus)
         if canv.widget_info:
@@ -186,9 +193,9 @@ def finalize_render(size, focus=False):
         canv.finalize(self, size, focus)
         return canv
     finalize_render.original_fn = fn
-    update_wrapper(finalize_render, fn)
     return finalize_render
 
+
 def cache_widget_rows(cls):
     """
     Return a function that wraps the cls.rows() method
@@ -196,6 +203,8 @@ def cache_widget_rows(cls):
     """
     ignore_focus = bool(getattr(cls, "ignore_focus", False))
     fn = cls.rows
+
+    @functools.wraps(fn)
     def cached_rows(self, size, focus=False):
         focus = focus and not ignore_focus
         canv = CanvasCache.fetch(self, cls, size, focus)
@@ -203,11 +212,10 @@ def cached_rows(self, size, focus=False):
             return canv.rows()
 
         return fn(self, size, focus)
-    update_wrapper(cached_rows, fn)
     return cached_rows
 
 
-class Widget(with_metaclass(WidgetMeta, object)):
+class Widget(metaclass=WidgetMeta):
     """
     Widget base class
 
@@ -544,30 +552,34 @@ def pack(self, size, focus=False):
             if FIXED in self.sizing():
                 raise NotImplementedError('Fixed widgets must override'
                     ' Widget.pack()')
-            raise WidgetError('Cannot pack () size, this is not a fixed'
-                ' widget: %s' % repr(self))
+            raise WidgetError(f'Cannot pack () size, this is not a fixed widget: {self!r}')
         elif len(size) == 1:
             if FLOW in self.sizing():
                 return size + (self.rows(size, focus),)
-            raise WidgetError('Cannot pack (maxcol,) size, this is not a'
-                ' flow widget: %s' % repr(self))
+            raise WidgetError(f'Cannot pack (maxcol,) size, this is not a flow widget: {self!r}')
         return size
 
-    base_widget = property(lambda self:self, doc="""
+    @property
+    def base_widget(self):
+        """
         Read-only property that steps through decoration widgets
         and returns the one at the base.  This default implementation
         returns self.
-        """)
+        """
+        return self
 
-    focus = property(lambda self:None, doc="""
+    @property
+    def focus(self):
+        """
         Read-only property returning the child widget in focus for
         container widgets.  This default implementation
         always returns ``None``, indicating that this widget has no children.
-        """)
+        """
+        return None
 
     def _not_a_container(self, val=None):
         raise IndexError(
-            "No focus_position, %r is not a container widget" % self)
+            f"No focus_position, {self!r} is not a container widget")
     focus_position = property(_not_a_container, _not_a_container, doc="""
         Property for reading and setting the focus position for
         container widgets. This default implementation raises
@@ -587,9 +599,7 @@ def _repr_words(self):
         if self.selectable():
             words = ["selectable"] + words
         if self.sizing() and self.sizing() != frozenset([FLOW, BOX, FIXED]):
-            sizing_modes = list(self.sizing())
-            sizing_modes.sort()
-            words.append("/".join(sizing_modes))
+            words.append("/".join(sorted(self.sizing())))
         return words + ["widget"]
 
     def _repr_attrs(self):
@@ -609,6 +619,17 @@ class FlowWidget(Widget):
     """
     _sizing = frozenset([FLOW])
 
+    def __init__(self, *args, **kwargs):
+        warnings.warn(
+            """
+            FlowWidget is deprecated. Inherit from Widget and add:
+
+                _sizing = frozenset(['flow'])
+
+            at the top of your class definition instead.""",
+            DeprecationWarning,
+        )
+
     def rows(self, size, focus=False):
         """
         All flow widgets must implement this function.
@@ -637,6 +658,18 @@ class BoxWidget(Widget):
     _selectable = True
     _sizing = frozenset([BOX])
 
+    def __init__(self, *args, **kwargs):
+        warnings.warn(
+            """
+            BoxWidget is deprecated. Inherit from Widget and add:
+
+                _sizing = frozenset(['box'])
+                _selectable = True
+
+            at the top of your class definition instead.""",
+            DeprecationWarning,
+        )
+
     def render(self, size, focus=False):
         """
         All widgets must implement this function.
@@ -651,8 +684,7 @@ def fixed_size(size):
     Used by FixedWidgets to test size parameter.
     """
     if size != ():
-        raise ValueError("FixedWidget takes only () for size." \
-            "passed: %r" % (size,))
+        raise ValueError(f"FixedWidget takes only () for size.passed: {size!r}")
 
 class FixedWidget(Widget):
     """
@@ -667,6 +699,17 @@ class FixedWidget(Widget):
     """
     _sizing = frozenset([FIXED])
 
+    def __init__(self, *args, **kwargs):
+        warnings.warn(
+            """
+            FixedWidget is deprecated. Inherit from Widget and add:
+
+                _sizing = frozenset(['fixed'])
+
+            at the top of your class definition instead.""",
+            DeprecationWarning,
+        )
+
     def render(self, size, focus=False):
         """
         All widgets must implement this function.
@@ -688,7 +731,7 @@ class Divider(Widget):
 
     ignore_focus = True
 
-    def __init__(self,div_char=u" ",top=0,bottom=0):
+    def __init__(self,div_char=" ",top=0,bottom=0):
         """
         :param div_char: character to repeat across line
         :type div_char: bytes or unicode
@@ -706,17 +749,17 @@ def __init__(self,div_char=u" ",top=0,bottom=0):
         >>> Divider(u'x', 1, 2)
         <Divider flow widget 'x' bottom=2 top=1>
         """
-        self.__super.__init__()
+        super().__init__()
         self.div_char = div_char
         self.top = top
         self.bottom = bottom
 
     def _repr_words(self):
-        return self.__super._repr_words() + [
-            python3_repr(self.div_char)] * (self.div_char != u" ")
+        return super()._repr_words() + [
+            repr(self.div_char)] * (self.div_char != " ")
 
     def _repr_attrs(self):
-        attrs = dict(self.__super._repr_attrs())
+        attrs = dict(super()._repr_attrs())
         if self.top: attrs['top'] = self.top
         if self.bottom: attrs['bottom'] = self.bottom
         return attrs
@@ -767,11 +810,11 @@ def __init__(self, fill_char=" "):
         >>> SolidFill(u'8')
         <SolidFill box widget '8'>
         """
-        self.__super.__init__()
+        super().__init__()
         self.fill_char = fill_char
 
     def _repr_words(self):
-        return self.__super._repr_words() + [python3_repr(self.fill_char)]
+        return super()._repr_words() + [repr(self.fill_char)]
 
     def render(self, size, focus=False ):
         """
@@ -829,7 +872,7 @@ def __init__(self, markup, align=LEFT, wrap=SPACE, layout=None):
         >>> t.attrib
         [('bold', 5)]
         """
-        self.__super.__init__()
+        super().__init__()
         self._cache_maxcol = None
         self.set_text(markup)
         self.set_layout(align, wrap, layout)
@@ -839,23 +882,23 @@ def _repr_words(self):
         Show the text in the repr in python3 format (b prefix for byte
         strings) and truncate if it's too long
         """
-        first = self.__super._repr_words()
+        first = super()._repr_words()
         text = self.get_text()[0]
-        rest = python3_repr(text)
+        rest = repr(text)
         if len(rest) > self._repr_content_length_max:
             rest = (rest[:self._repr_content_length_max * 2 // 3 - 3] +
                 '...' + rest[-self._repr_content_length_max // 3:])
         return first + [rest]
 
     def _repr_attrs(self):
-        attrs = dict(self.__super._repr_attrs(),
+        attrs = dict(super()._repr_attrs(),
             align=self._align_mode,
             wrap=self._wrap_mode)
         return remove_defaults(attrs, Text.__init__)
 
     def _invalidate(self):
         self._cache_maxcol = None
-        self.__super._invalidate()
+        super()._invalidate()
 
     def set_text(self,markup):
         """
@@ -897,14 +940,21 @@ def get_text(self):
         """
         return self._text, self._attrib
 
-    text = property(lambda self:self.get_text()[0], doc="""
+    @property
+    def text(self):
+        """
         Read-only property returning the complete bytes/unicode content
         of this widget
-        """)
-    attrib = property(lambda self:self.get_text()[1], doc="""
+        """
+        return self.get_text()[0]
+
+    @property
+    def attrib(self):
+        """
         Read-only property returning the run-length encoded display
         attributes of this widget
-        """)
+        """
+        return self.get_text()[1]
 
     def set_align_mode(self, mode):
         """
@@ -928,8 +978,7 @@ def set_align_mode(self, mode):
         TextError: Alignment mode 'somewhere' not supported.
         """
         if not self.layout.supports_align_mode(mode):
-            raise TextError("Alignment mode %r not supported."%
-                (mode,))
+            raise TextError(f"Alignment mode {mode!r} not supported.")
         self._align_mode = mode
         self._invalidate()
 
@@ -957,7 +1006,7 @@ def set_wrap_mode(self, mode):
         TextError: Wrap mode 'somehow' not supported.
         """
         if not self.layout.supports_wrap_mode(mode):
-            raise TextError("Wrap mode %r not supported."%(mode,))
+            raise TextError(f"Wrap mode {mode!r} not supported.")
         self._wrap_mode = mode
         self._invalidate()
 
@@ -985,7 +1034,10 @@ def set_layout(self, align, wrap, layout=None):
 
     align = property(lambda self:self._align_mode, set_align_mode)
     wrap = property(lambda self:self._wrap_mode, set_wrap_mode)
-    layout = property(lambda self:self._layout)
+
+    @property
+    def layout(self):
+        return self._layout
 
     def render(self, size, focus=False):
         """
@@ -1123,7 +1175,7 @@ def valid_char(self, ch):
         """
         return is_wide_char(ch,0) or (len(ch)==1 and ord(ch) >= 32)
 
-    def __init__(self, caption=u"", edit_text=u"", multiline=False,
+    def __init__(self, caption="", edit_text="", multiline=False,
             align=LEFT, wrap=SPACE, allow_tab=False,
             edit_pos=None, layout=None, mask=None):
         """
@@ -1158,7 +1210,7 @@ def __init__(self, caption=u"", edit_text=u"", multiline=False,
         <Edit selectable flow widget '3.14' align='right' edit_pos=4>
         """
 
-        self.__super.__init__("", align, wrap, layout)
+        super().__init__("", align, wrap, layout)
         self.multiline = multiline
         self.allow_tab = allow_tab
         self._edit_pos = 0
@@ -1172,13 +1224,13 @@ def __init__(self, caption=u"", edit_text=u"", multiline=False,
         self._shift_view_to_cursor = False
 
     def _repr_words(self):
-        return self.__super._repr_words()[:-1] + [
-            python3_repr(self._edit_text)] + [
-            'caption=' + python3_repr(self._caption)] * bool(self._caption) + [
+        return super()._repr_words()[:-1] + [
+            repr(self._edit_text)] + [
+            f"caption={self._caption!r}"] * bool(self._caption) + [
             'multiline'] * (self.multiline is True)
 
     def _repr_attrs(self):
-        attrs = dict(self.__super._repr_attrs(),
+        attrs = dict(super()._repr_attrs(),
             edit_pos=self._edit_pos)
         return remove_defaults(attrs, Edit.__init__)
 
@@ -1293,9 +1345,12 @@ def set_caption(self, caption):
         self._caption, self._attrib = decompose_tagmarkup(caption)
         self._invalidate()
 
-    caption = property(lambda self:self._caption, doc="""
+    @property
+    def caption(self):
+        """
         Read-only property returning the caption for this widget.
-        """)
+        """
+        return self._caption
 
     def set_edit_pos(self, pos):
         """
@@ -1416,8 +1471,8 @@ def _normalize_to_caption(self, text):
         Return text converted to the same type as self.caption
         (bytes or unicode)
         """
-        tu = isinstance(text, text_type)
-        cu = isinstance(self._caption, text_type)
+        tu = isinstance(text, str)
+        cu = isinstance(self._caption, str)
         if tu == cu:
             return text
         if tu:
@@ -1475,8 +1530,8 @@ def keypress(self, size, key):
 
         p = self.edit_pos
         if self.valid_char(key):
-            if (isinstance(key, text_type) and not
-                    isinstance(self._caption, text_type)):
+            if (isinstance(key, str) and not
+                    isinstance(self._caption, str)):
                 # screen is sending us unicode input, must be using utf-8
                 # encoding because that's all we support, so convert it
                 # to bytes to match our caption's type
@@ -1701,7 +1756,7 @@ def __init__(self,caption="",default=None):
         """
         if default is not None: val = str(default)
         else: val = ""
-        self.__super.__init__(caption,val)
+        super().__init__(caption,val)
 
     def keypress(self, size, key):
         """
@@ -1761,17 +1816,42 @@ def render(self, size, focus=False):
             canv = get_delegate(self).render(size, focus=focus)
             return CompositeCanvas(canv)
 
-        selectable = property(lambda self:get_delegate(self).selectable)
-        get_cursor_coords = property(
-            lambda self:get_delegate(self).get_cursor_coords)
-        get_pref_col = property(lambda self:get_delegate(self).get_pref_col)
-        keypress = property(lambda self:get_delegate(self).keypress)
-        move_cursor_to_coords = property(
-            lambda self:get_delegate(self).move_cursor_to_coords)
-        rows = property(lambda self:get_delegate(self).rows)
-        mouse_event = property(lambda self:get_delegate(self).mouse_event)
-        sizing = property(lambda self:get_delegate(self).sizing)
-        pack = property(lambda self:get_delegate(self).pack)
+        @property
+        def selectable(self):
+            return get_delegate(self).selectable
+
+        @property
+        def get_cursor_coords(self):
+            return get_delegate(self).get_cursor_coords
+
+        @property
+        def get_pref_col(self):
+            return get_delegate(self).get_pref_col
+
+        @property
+        def keypress(self):
+            return get_delegate(self).keypress
+
+        @property
+        def move_cursor_to_coords(self):
+            return get_delegate(self).move_cursor_to_coords
+
+        @property
+        def rows(self):
+            return get_delegate(self).rows
+
+        @property
+        def mouse_event(self):
+            return get_delegate(self).mouse_event
+
+        @property
+        def sizing(self):
+            return get_delegate(self).sizing
+
+        @property
+        def pack(self):
+            return get_delegate(self).pack
+
     return DelegateToWidgetMixin
 
 
diff --git a/urwid/wimp.py b/urwid/wimp.py
index e7ac9c9ef..622af1d22 100755
--- a/urwid/wimp.py
+++ b/urwid/wimp.py
@@ -17,21 +17,21 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
-from __future__ import division, print_function
 
-from urwid.widget import (Text, WidgetWrap, delegate_to_widget_mixin, BOX,
-    FLOW)
+from __future__ import annotations
+
 from urwid.canvas import CompositeCanvas
-from urwid.signals import connect_signal
+from urwid.command_map import ACTIVATE
 from urwid.container import Columns, Overlay
-from urwid.util import is_mouse_press
-from urwid.text_layout import calc_coords
-from urwid.signals import disconnect_signal # doctests
-from urwid.split_repr import python3_repr
 from urwid.decoration import WidgetDecoration
-from urwid.command_map import ACTIVATE
+from urwid.signals import disconnect_signal  # doctests
+from urwid.signals import connect_signal
+from urwid.text_layout import calc_coords
+from urwid.util import is_mouse_press
+from urwid.widget import BOX, FLOW, Text, WidgetWrap, delegate_to_widget_mixin
+
 
 class SelectableIcon(Text):
     ignore_focus = False
@@ -47,7 +47,7 @@ def __init__(self, text, cursor_position=0):
         displayed at a fixed location in the text when in focus.
         This widget has no special handling of keyboard or mouse input.
         """
-        self.__super.__init__(text)
+        super().__init__(text)
         self._cursor_position = cursor_position
 
     def render(self, size, focus=False):
@@ -66,7 +66,7 @@ def render(self, size, focus=False):
         >>> si.render((2,), focus=True).cursor
         (0, 1)
         """
-        c = self.__super.render(size, focus)
+        c = super().render(size, focus)
         if focus:
             # create a new canvas so we can add a cursor
             c = CompositeCanvas(c)
@@ -145,12 +145,12 @@ def __init__(self, label, state=False, has_mixed=False,
         >>> cb.render((20,), focus=True).text # ... = b in Python 3
         [...'[X] Extra onions    ']
         """
-        self.__super.__init__(None) # self.w set by set_state below
+        super().__init__(None) # self.w set by set_state below
         self._label = Text("")
         self.has_mixed = has_mixed
         self._state = None
         if checked_symbol:
-            self.states[True] = SelectableIcon(u"[%s]" % checked_symbol, 1)
+            self.states[True] = SelectableIcon(f"[{checked_symbol}]", 1)
         # The old way of listening for a change was to pass the callback
         # in to the constructor.  Just convert it to the new way:
         if on_state_change:
@@ -159,11 +159,11 @@ def __init__(self, label, state=False, has_mixed=False,
         self.set_state(state)
 
     def _repr_words(self):
-        return self.__super._repr_words() + [
-            python3_repr(self.label)]
+        return super()._repr_words() + [
+            repr(self.label)]
 
     def _repr_attrs(self):
-        return dict(self.__super._repr_attrs(),
+        return dict(super()._repr_attrs(),
             state=self.state)
 
     def set_label(self, label):
@@ -233,8 +233,7 @@ def set_state(self, state, do_callback=True):
             return
 
         if state not in self.states:
-            raise CheckBoxError("%s Invalid state: %s" % (
-                repr(self), repr(state)))
+            raise CheckBoxError(f"{self!r} Invalid state: {state!r}")
 
         # self._state is None is a special case when the CheckBox
         # has just been created
@@ -370,7 +369,7 @@ def __init__(self, group, label, state="first True",
             state = not group
 
         self.group = group
-        self.__super.__init__(label, state, False, on_state_change,
+        super().__init__(label, state, False, on_state_change,
             user_data)
         group.append(self)
 
@@ -408,7 +407,7 @@ def set_state(self, state, do_callback=True):
         if self._state == state:
             return
 
-        self.__super.set_state(state, do_callback)
+        super().set_state(state, do_callback)
 
         # if we're clearing the state we don't have to worry about
         # other buttons in the button group
@@ -480,7 +479,7 @@ def __init__(self, label, on_press=None, user_data=None):
             self._label,
             ('fixed', 1, self.button_right)],
             dividechars=1)
-        self.__super.__init__(cols)
+        super().__init__(cols)
 
         # The old way of listening for a change was to pass the callback
         # in to the constructor.  Just convert it to the new way:
@@ -491,8 +490,8 @@ def __init__(self, label, on_press=None, user_data=None):
 
     def _repr_words(self):
         # include button.label in repr(button)
-        return self.__super._repr_words() + [
-            python3_repr(self.label)]
+        return super()._repr_words() + [
+            repr(self.label)]
 
     def set_label(self, label):
         """
@@ -566,10 +565,9 @@ def mouse_event(self, size, event, button, x, y, focus):
         return True
 
 
-class PopUpLauncher(delegate_to_widget_mixin('_original_widget'),
-                    WidgetDecoration):
+class PopUpLauncher(delegate_to_widget_mixin('_original_widget'), WidgetDecoration):
     def __init__(self, original_widget):
-        self.__super.__init__(original_widget)
+        super().__init__(original_widget)
         self._pop_up_widget = None
 
     def create_pop_up(self, *args, **kwargs):
@@ -599,7 +597,7 @@ def close_pop_up(self):
         self._invalidate()
 
     def render(self, size, focus=False):
-        canv = self.__super.render(size, focus)
+        canv = super().render(size, focus)
         if self._pop_up_widget:
             canv = CompositeCanvas(canv)
             canv.set_pop_up(self._pop_up_widget, **self.get_pop_up_parameters(size, focus))
@@ -609,11 +607,11 @@ def render(self, size, focus=False):
 class PopUpTarget(WidgetDecoration):
     # FIXME: this whole class is a terrible hack and must be fixed
     # when layout and rendering are separated
-    _sizing = set([BOX])
+    _sizing = {BOX}
     _selectable = True
 
     def __init__(self, original_widget):
-        self.__super.__init__(original_widget)
+        super().__init__(original_widget)
         self._pop_up = None
         self._current_widget = self._original_widget
 


Test patch:
diff --git a/examples/input_test.py b/examples/input_test.py
index 0a8eed9cf..121adc4f3 100755
--- a/examples/input_test.py
+++ b/examples/input_test.py
@@ -17,18 +17,20 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
 """
 Keyboard test application
 """
 
+from __future__ import annotations
+
+import sys
+
+import urwid
 import urwid.curses_display
 import urwid.raw_display
 import urwid.web_display
-import urwid
-
-import sys
 
 if urwid.web_display.is_web_request():
     Screen = urwid.web_display.Screen
diff --git a/examples/palette_test.py b/examples/palette_test.py
index 4fa5d19ee..d8cbfbc87 100755
--- a/examples/palette_test.py
+++ b/examples/palette_test.py
@@ -17,15 +17,16 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
 
 """
 Palette test.  Shows the available foreground and background settings
 in monochrome, 16 color, 88 color, 256 color, and 24-bit (true) color modes.
 """
 
+from __future__ import annotations
+
 import re
-import sys
 
 import urwid
 import urwid.raw_display
@@ -281,12 +282,12 @@ def parse_chart(chart, convert):
                 entry = entry[elen:].strip()
             else: # try the whole thing
                 attrtext = convert(entry.strip())
-                assert attrtext, "Invalid palette entry: %r" % entry
+                assert attrtext, f"Invalid palette entry: {entry!r}"
                 elen = len(entry)
                 entry = ""
             attr, text = attrtext
             if chart == CHART_TRUE:
-                out.append((attr, u"\u2584"))
+                out.append((attr, "\u2584"))
             else:
                 out.append((attr, text.ljust(elen)))
     return out
diff --git a/urwid/tests/test_canvas.py b/urwid/tests/test_canvas.py
index 0391593a6..5a1d454cd 100644
--- a/urwid/tests/test_canvas.py
+++ b/urwid/tests/test_canvas.py
@@ -1,8 +1,9 @@
+from __future__ import annotations
+
 import unittest
 
-from urwid import canvas
-from urwid.compat import B
 import urwid
+from urwid import canvas
 
 
 class CanvasCacheTest(unittest.TestCase):
@@ -12,7 +13,7 @@ def setUp(self):
 
     def cct(self, widget, size, focus, expected):
         got = urwid.CanvasCache.fetch(widget, urwid.Widget, size, focus)
-        assert expected==got, "got: %s expected: %s"%(got, expected)
+        assert expected==got, f"got: {got} expected: {expected}"
 
     def test1(self):
         a = urwid.Text("")
@@ -42,51 +43,49 @@ def test1(self):
 
 class CanvasTest(unittest.TestCase):
     def ct(self, text, attr, exp_content):
-        c = urwid.TextCanvas([B(t) for t in text], attr)
+        c = urwid.TextCanvas([t.encode('iso8859-1') for t in text], attr)
         content = list(c.content())
-        assert content == exp_content, "got: %r expected: %r" % (content,
-                                                                 exp_content)
+        assert content == exp_content, f"got: {content!r} expected: {exp_content!r}"
 
     def ct2(self, text, attr, left, top, cols, rows, def_attr, exp_content):
-        c = urwid.TextCanvas([B(t) for t in text], attr)
+        c = urwid.TextCanvas([t.encode('iso8859-1') for t in text], attr)
         content = list(c.content(left, top, cols, rows, def_attr))
-        assert content == exp_content, "got: %r expected: %r" % (content,
-                                                                 exp_content)
+        assert content == exp_content, f"got: {content!r} expected: {exp_content!r}"
 
     def test1(self):
-        self.ct(["Hello world"], None, [[(None, None, B("Hello world"))]])
+        self.ct(["Hello world"], None, [[(None, None, b"Hello world")]])
         self.ct(["Hello world"], [[("a",5)]],
-            [[("a", None, B("Hello")), (None, None, B(" world"))]])
+            [[("a", None, b"Hello"), (None, None, b" world")]])
         self.ct(["Hi","There"], None,
-            [[(None, None, B("Hi   "))], [(None, None, B("There"))]])
+            [[(None, None, b"Hi   ")], [(None, None, b"There")]])
 
     def test2(self):
         self.ct2(["Hello"], None, 0, 0, 5, 1, None,
-            [[(None, None, B("Hello"))]])
+            [[(None, None, b"Hello")]])
         self.ct2(["Hello"], None, 1, 0, 4, 1, None,
-            [[(None, None, B("ello"))]])
+            [[(None, None, b"ello")]])
         self.ct2(["Hello"], None, 0, 0, 4, 1, None,
-            [[(None, None, B("Hell"))]])
+            [[(None, None, b"Hell")]])
         self.ct2(["Hi","There"], None, 1, 0, 3, 2, None,
-            [[(None, None, B("i  "))], [(None, None, B("her"))]])
+            [[(None, None, b"i  ")], [(None, None, b"her")]])
         self.ct2(["Hi","There"], None, 0, 0, 5, 1, None,
-            [[(None, None, B("Hi   "))]])
+            [[(None, None, b"Hi   ")]])
         self.ct2(["Hi","There"], None, 0, 1, 5, 1, None,
-            [[(None, None, B("There"))]])
+            [[(None, None, b"There")]])
 
 
 class ShardBodyTest(unittest.TestCase):
     def sbt(self, shards, shard_tail, expected):
         result = canvas.shard_body(shards, shard_tail, False)
-        assert result == expected, "got: %r expected: %r" % (result, expected)
+        assert result == expected, f"got: {result!r} expected: {expected!r}"
 
     def sbttail(self, num_rows, sbody, expected):
         result = canvas.shard_body_tail(num_rows, sbody)
-        assert result == expected, "got: %r expected: %r" % (result, expected)
+        assert result == expected, f"got: {result!r} expected: {expected!r}"
 
     def sbtrow(self, sbody, expected):
         result = list(canvas.shard_body_row(sbody))
-        assert result == expected, "got: %r expected: %r" % (result, expected)
+        assert result == expected, f"got: {result!r} expected: {expected!r}"
 
 
     def test1(self):
@@ -138,15 +137,15 @@ def test3(self):
 class ShardsTrimTest(unittest.TestCase):
     def sttop(self, shards, top, expected):
         result = canvas.shards_trim_top(shards, top)
-        assert result == expected, "got: %r expected: %r" % (result, expected)
+        assert result == expected, f"got: {result!r} expected: {expected!r}"
 
     def strows(self, shards, rows, expected):
         result = canvas.shards_trim_rows(shards, rows)
-        assert result == expected, "got: %r expected: %r" % (result, expected)
+        assert result == expected, f"got: {result!r} expected: {expected!r}"
 
     def stsides(self, shards, left, cols, expected):
         result = canvas.shards_trim_sides(shards, left, cols)
-        assert result == expected, "got: %r expected: %r" % (result, expected)
+        assert result == expected, f"got: {result!r} expected: {expected!r}"
 
 
     def test1(self):
@@ -229,7 +228,7 @@ def test2(self):
 class ShardsJoinTest(unittest.TestCase):
     def sjt(self, shard_lists, expected):
         result = canvas.shards_join(shard_lists)
-        assert result == expected, "got: %r expected: %r" % (result, expected)
+        assert result == expected, f"got: {result!r} expected: {expected!r}"
 
     def test(self):
         shards1 = [(5, [(0,0,10,5,None,"foo"), (0,0,5,8,None,"baz")]),
@@ -263,129 +262,126 @@ def cjtest(self, desc, l, expected):
         l = [(c, None, False, n) for c, n in l]
         result = list(urwid.CanvasJoin(l).content())
 
-        assert result == expected, "%s expected %r, got %r"%(
-            desc, expected, result)
+        assert result == expected, f"{desc} expected {expected!r}, got {result!r}"
 
     def test(self):
         C = urwid.TextCanvas
-        hello = C([B("hello")])
-        there = C([B("there")], [[("a",5)]])
-        a = C([B("a")])
-        hi = C([B("hi")])
-        how = C([B("how")], [[("a",1)]])
-        dy = C([B("dy")])
-        how_you = C([B("how"), B("you")])
+        hello = C([b"hello"])
+        there = C([b"there"], [[("a",5)]])
+        a = C([b"a"])
+        hi = C([b"hi"])
+        how = C([b"how"], [[("a",1)]])
+        dy = C([b"dy"])
+        how_you = C([b"how", b"you"])
 
         self.cjtest("one", [(hello, 5)],
-            [[(None, None, B("hello"))]])
+            [[(None, None, b"hello")]])
         self.cjtest("two", [(hello, 5), (there, 5)],
-            [[(None, None, B("hello")), ("a", None, B("there"))]])
+            [[(None, None, b"hello"), ("a", None, b"there")]])
         self.cjtest("two space", [(hello, 7), (there, 5)],
-            [[(None, None, B("hello")),(None,None,B("  ")),
-            ("a", None, B("there"))]])
+            [[(None, None, b"hello"),(None,None,b"  "),
+            ("a", None, b"there")]])
         self.cjtest("three space", [(hi, 4), (how, 3), (dy, 2)],
-            [[(None, None, B("hi")),(None,None,B("  ")),("a",None, B("h")),
-            (None,None,B("ow")),(None,None,B("dy"))]])
+            [[(None, None, b"hi"),(None,None,b"  "),("a",None, b"h"),
+            (None,None,b"ow"),(None,None,b"dy")]])
         self.cjtest("four space", [(a, 2), (hi, 3), (dy, 3), (a, 1)],
-            [[(None, None, B("a")),(None,None,B(" ")),
-            (None, None, B("hi")),(None,None,B(" ")),
-            (None, None, B("dy")),(None,None,B(" ")),
-            (None, None, B("a"))]])
+            [[(None, None, b"a"),(None,None,b" "),
+            (None, None, b"hi"),(None,None,b" "),
+            (None, None, b"dy"),(None,None,b" "),
+            (None, None, b"a")]])
         self.cjtest("pile 2", [(how_you, 4), (hi, 2)],
-            [[(None, None, B('how')), (None, None, B(' ')),
-            (None, None, B('hi'))],
-            [(None, None, B('you')), (None, None, B(' ')),
-            (None, None, B('  '))]])
+            [[(None, None, b'how'), (None, None, b' '),
+            (None, None, b'hi')],
+            [(None, None, b'you'), (None, None, b' '),
+            (None, None, b'  ')]])
         self.cjtest("pile 2r", [(hi, 4), (how_you, 3)],
-            [[(None, None, B('hi')), (None, None, B('  ')),
-            (None, None, B('how'))],
-            [(None, None, B('    ')),
-            (None, None, B('you'))]])
+            [[(None, None, b'hi'), (None, None, b'  '),
+            (None, None, b'how')],
+            [(None, None, b'    '),
+            (None, None, b'you')]])
 
 
 class CanvasOverlayTest(unittest.TestCase):
     def cotest(self, desc, bgt, bga, fgt, fga, l, r, et):
-        bgt = B(bgt)
-        fgt = B(fgt)
+        bgt = bgt.encode('iso8859-1')
+        fgt = fgt.encode('iso8859-1')
         bg = urwid.CompositeCanvas(
             urwid.TextCanvas([bgt],[bga]))
         fg = urwid.CompositeCanvas(
             urwid.TextCanvas([fgt],[fga]))
         bg.overlay(fg, l, 0)
         result = list(bg.content())
-        assert result == et, "%s expected %r, got %r"%(
-            desc, et, result)
+        assert result == et, f"{desc} expected {et!r}, got {result!r}"
 
     def test1(self):
         self.cotest("left", "qxqxqxqx", [], "HI", [], 0, 6,
-            [[(None, None, B("HI")),(None,None,B("qxqxqx"))]])
+            [[(None, None, b"HI"),(None,None,b"qxqxqx")]])
         self.cotest("right", "qxqxqxqx", [], "HI", [], 6, 0,
-            [[(None, None, B("qxqxqx")),(None,None,B("HI"))]])
+            [[(None, None, b"qxqxqx"),(None,None,b"HI")]])
         self.cotest("center", "qxqxqxqx", [], "HI", [], 3, 3,
-            [[(None, None, B("qxq")),(None,None,B("HI")),
-            (None,None,B("xqx"))]])
+            [[(None, None, b"qxq"),(None,None,b"HI"),
+            (None,None,b"xqx")]])
         self.cotest("center2", "qxqxqxqx", [], "HI  ", [], 2, 2,
-            [[(None, None, B("qx")),(None,None,B("HI  ")),
-            (None,None,B("qx"))]])
+            [[(None, None, b"qx"),(None,None,b"HI  "),
+            (None,None,b"qx")]])
         self.cotest("full", "rz", [], "HI", [], 0, 0,
-            [[(None, None, B("HI"))]])
+            [[(None, None, b"HI")]])
 
     def test2(self):
         self.cotest("same","asdfghjkl",[('a',9)],"HI",[('a',2)],4,3,
-            [[('a',None,B("asdf")),('a',None,B("HI")),('a',None,B("jkl"))]])
+            [[('a',None,b"asdf"),('a',None,b"HI"),('a',None,b"jkl")]])
         self.cotest("diff","asdfghjkl",[('a',9)],"HI",[('b',2)],4,3,
-            [[('a',None,B("asdf")),('b',None,B("HI")),('a',None,B("jkl"))]])
+            [[('a',None,b"asdf"),('b',None,b"HI"),('a',None,b"jkl")]])
         self.cotest("None end","asdfghjkl",[('a',9)],"HI  ",[('a',2)],
             2,3,
-            [[('a',None,B("as")),('a',None,B("HI")),
-            (None,None,B("  ")),('a',None,B("jkl"))]])
+            [[('a',None,b"as"),('a',None,b"HI"),
+            (None,None,b"  "),('a',None,b"jkl")]])
         self.cotest("float end","asdfghjkl",[('a',3)],"HI",[('a',2)],
             4,3,
-            [[('a',None,B("asd")),(None,None,B("f")),
-            ('a',None,B("HI")),(None,None,B("jkl"))]])
+            [[('a',None,b"asd"),(None,None,b"f"),
+            ('a',None,b"HI"),(None,None,b"jkl")]])
         self.cotest("cover 2","asdfghjkl",[('a',5),('c',4)],"HI",
             [('b',2)],4,3,
-            [[('a',None,B("asdf")),('b',None,B("HI")),('c',None,B("jkl"))]])
+            [[('a',None,b"asdf"),('b',None,b"HI"),('c',None,b"jkl")]])
         self.cotest("cover 2-2","asdfghjkl",
             [('a',4),('d',1),('e',1),('c',3)],
             "HI",[('b',2)], 4, 3,
-            [[('a',None,B("asdf")),('b',None,B("HI")),('c',None,B("jkl"))]])
+            [[('a',None,b"asdf"),('b',None,b"HI"),('c',None,b"jkl")]])
 
     def test3(self):
         urwid.set_encoding("euc-jp")
         self.cotest("db0","\xA1\xA1\xA1\xA1\xA1\xA1",[],"HI",[],2,2,
-            [[(None,None,B("\xA1\xA1")),(None,None,B("HI")),
-            (None,None,B("\xA1\xA1"))]])
+            [[(None,None,b"\xA1\xA1"),(None,None,b"HI"),
+            (None,None,b"\xA1\xA1")]])
         self.cotest("db1","\xA1\xA1\xA1\xA1\xA1\xA1",[],"OHI",[],1,2,
-            [[(None,None,B(" ")),(None,None,B("OHI")),
-            (None,None,B("\xA1\xA1"))]])
+            [[(None,None,b" "),(None,None,b"OHI"),
+            (None,None,b"\xA1\xA1")]])
         self.cotest("db2","\xA1\xA1\xA1\xA1\xA1\xA1",[],"OHI",[],2,1,
-            [[(None,None,B("\xA1\xA1")),(None,None,B("OHI")),
-            (None,None,B(" "))]])
+            [[(None,None,b"\xA1\xA1"),(None,None,b"OHI"),
+            (None,None,b" ")]])
         self.cotest("db3","\xA1\xA1\xA1\xA1\xA1\xA1",[],"OHIO",[],1,1,
-            [[(None,None,B(" ")),(None,None,B("OHIO")),(None,None,B(" "))]])
+            [[(None,None,b" "),(None,None,b"OHIO"),(None,None,b" ")]])
 
 
 class CanvasPadTrimTest(unittest.TestCase):
     def cptest(self, desc, ct, ca, l, r, et):
-        ct = B(ct)
+        ct = ct.encode('iso8859-1')
         c = urwid.CompositeCanvas(
             urwid.TextCanvas([ct], [ca]))
         c.pad_trim_left_right(l, r)
         result = list(c.content())
-        assert result == et, "%s expected %r, got %r"%(
-            desc, et, result)
+        assert result == et, f"{desc} expected {et!r}, got {result!r}"
 
     def test1(self):
         self.cptest("none", "asdf", [], 0, 0,
-            [[(None,None,B("asdf"))]])
+            [[(None,None,b"asdf")]])
         self.cptest("left pad", "asdf", [], 2, 0,
-            [[(None,None,B("  ")),(None,None,B("asdf"))]])
+            [[(None,None,b"  "),(None,None,b"asdf")]])
         self.cptest("right pad", "asdf", [], 0, 2,
-            [[(None,None,B("asdf")),(None,None,B("  "))]])
+            [[(None,None,b"asdf"),(None,None,b"  ")]])
 
     def test2(self):
         self.cptest("left trim", "asdf", [], -2, 0,
-            [[(None,None,B("df"))]])
+            [[(None,None,b"df")]])
         self.cptest("right trim", "asdf", [], 0, -2,
-            [[(None,None,B("as"))]])
+            [[(None,None,b"as")]])
diff --git a/urwid/tests/test_container.py b/urwid/tests/test_container.py
index 8b693b7d3..f5a9182d4 100644
--- a/urwid/tests/test_container.py
+++ b/urwid/tests/test_container.py
@@ -1,13 +1,10 @@
+from __future__ import annotations
+
 import unittest
-try:
-    # Python3 version
-    import unittest.mock as mock
-except ImportError:
-    # Python2, rely on PyPi
-    import mock
+from unittest import mock
 
-from urwid.tests.util import SelectableText
 import urwid
+from urwid.tests.util import SelectableText
 
 
 class FrameTest(unittest.TestCase):
@@ -32,8 +29,7 @@ def rows(self, size, focus=False):
 
         rval = f.frame_top_bottom(size, focus)
         exp = (top, bottom), (header_rows, footer_rows)
-        assert exp == rval, "%s expected %r but got %r"%(
-            desc,exp,rval)
+        assert exp == rval, f"{desc} expected {exp!r} but got {rval!r}"
 
     def test(self):
         self.ftbtest("simple", 'body', 0, 0, (9, 10), True, 0, 0)
@@ -71,15 +67,13 @@ def ktest(self, desc, l, focus_item, key,
             rkey, rfocus, rpref_col):
         p = urwid.Pile( l, focus_item )
         rval = p.keypress( (20,), key )
-        assert rkey == rval, "%s key expected %r but got %r" %(
-            desc, rkey, rval)
+        assert rkey == rval, f"{desc} key expected {rkey!r} but got {rval!r}"
         new_focus = l.index(p.get_focus())
         assert new_focus == rfocus, "%s focus expected %r but got %r" %(
             desc, rfocus, new_focus)
         new_pref = p.get_pref_col((20,))
         assert new_pref == rpref_col, (
-            "%s pref_col expected %r but got %r" % (
-            desc, rpref_col, new_pref))
+            f"{desc} pref_col expected {rpref_col!r} but got {new_pref!r}")
 
     def test_select_change(self):
         T,S,E = urwid.Text, SelectableText, urwid.Edit
@@ -145,7 +139,7 @@ class ColumnsTest(unittest.TestCase):
     def cwtest(self, desc, l, divide, size, exp, focus_column=0):
         c = urwid.Columns(l, divide, focus_column)
         rval = c.column_widths( size )
-        assert rval == exp, "%s expected %s, got %s"%(desc,exp,rval)
+        assert rval == exp, f"{desc} expected {exp}, got {rval}"
 
     def test_widths(self):
         x = urwid.Text("") # sample "column"
@@ -207,12 +201,11 @@ def test_widths_focus_end(self):
     def mctest(self, desc, l, divide, size, col, row, exp, f_col, pref_col):
         c = urwid.Columns( l, divide )
         rval = c.move_cursor_to_coords( size, col, row )
-        assert rval == exp, "%s expected %r, got %r"%(desc,exp,rval)
+        assert rval == exp, f"{desc} expected {exp!r}, got {rval!r}"
         assert c.focus_col == f_col, "%s expected focus_col %s got %s"%(
             desc, f_col, c.focus_col)
         pc = c.get_pref_col( size )
-        assert pc == pref_col, "%s expected pref_col %s, got %s"%(
-            desc, pref_col, pc)
+        assert pc == pref_col, f"{desc} expected pref_col {pref_col}, got {pc}"
 
     def test_move_cursor(self):
         e, s, x = urwid.Edit("",""),SelectableText(""), urwid.Text("")
@@ -244,7 +237,7 @@ def test_init_with_a_generator(self):
         urwid.Columns(urwid.Text(c) for c in "ABC")
 
     def test_old_attributes(self):
-        c = urwid.Columns([urwid.Text(u'a'), urwid.SolidFill(u'x')],
+        c = urwid.Columns([urwid.Text('a'), urwid.SolidFill('x')],
             box_columns=[1])
         self.assertEqual(c.box_columns, [1])
         c.box_columns=[]
@@ -263,13 +256,13 @@ def test_box_column(self):
 
 class OverlayTest(unittest.TestCase):
     def test_old_params(self):
-        o1 = urwid.Overlay(urwid.SolidFill(u'X'), urwid.SolidFill(u'O'),
+        o1 = urwid.Overlay(urwid.SolidFill('X'), urwid.SolidFill('O'),
             ('fixed left', 5), ('fixed right', 4),
             ('fixed top', 3), ('fixed bottom', 2),)
         self.assertEqual(o1.contents[1][1], (
             'left', None, 'relative', 100, None, 5, 4,
             'top', None, 'relative', 100, None, 3, 2))
-        o2 = urwid.Overlay(urwid.SolidFill(u'X'), urwid.SolidFill(u'O'),
+        o2 = urwid.Overlay(urwid.SolidFill('X'), urwid.SolidFill('O'),
             ('fixed right', 5), ('fixed left', 4),
             ('fixed bottom', 3), ('fixed top', 2),)
         self.assertEqual(o2.contents[1][1], (
@@ -278,7 +271,7 @@ def test_old_params(self):
 
     def test_get_cursor_coords(self):
         self.assertEqual(urwid.Overlay(urwid.Filler(urwid.Edit()),
-            urwid.SolidFill(u'B'),
+            urwid.SolidFill('B'),
             'right', 1, 'bottom', 1).get_cursor_coords((2,2)), (1,1))
 
 
@@ -374,15 +367,15 @@ def test_columns(self):
         self.wstest(urwid.Columns([(4, urwid.SolidFill())]))
 
     def test_buttons(self):
-        self.fwstest(urwid.Button(u"hello"))
-        self.fwstest(urwid.RadioButton([], u"hello"))
+        self.fwstest(urwid.Button("hello"))
+        self.fwstest(urwid.RadioButton([], "hello"))
 
 
 class CommonContainerTest(unittest.TestCase):
     def test_pile(self):
-        t1 = urwid.Text(u'one')
-        t2 = urwid.Text(u'two')
-        t3 = urwid.Text(u'three')
+        t1 = urwid.Text('one')
+        t2 = urwid.Text('two')
+        t3 = urwid.Text('three')
         sf = urwid.SolidFill('x')
         p = urwid.Pile([])
         self.assertEqual(p.focus, None)
@@ -439,9 +432,9 @@ def test_pile(self):
         self.assertEqual(len(p.contents), 1)
 
     def test_columns(self):
-        t1 = urwid.Text(u'one')
-        t2 = urwid.Text(u'two')
-        t3 = urwid.Text(u'three')
+        t1 = urwid.Text('one')
+        t2 = urwid.Text('two')
+        t3 = urwid.Text('three')
         sf = urwid.SolidFill('x')
         c = urwid.Columns([])
         self.assertEqual(c.focus, None)
@@ -527,8 +520,8 @@ def test_list_box(self):
             None))
         self.assertRaises(IndexError, lambda: setattr(lb, 'focus_position', 0))
 
-        t1 = urwid.Text(u'one')
-        t2 = urwid.Text(u'two')
+        t1 = urwid.Text('one')
+        t2 = urwid.Text('two')
         lb = urwid.ListBox(urwid.SimpleListWalker([t1, t2]))
         self.assertEqual(lb.focus, t1)
         self.assertEqual(lb.focus_position, 0)
@@ -552,8 +545,8 @@ def test_grid_flow(self):
         self.assertRaises(urwid.GridFlowError, lambda: gf.options(
             'pack', None))
 
-        t1 = urwid.Text(u'one')
-        t2 = urwid.Text(u'two')
+        t1 = urwid.Text('one')
+        t2 = urwid.Text('two')
         gf = urwid.GridFlow([t1, t2], 5, 1, 0, 'left')
         self.assertEqual(gf.focus, t1)
         self.assertEqual(gf.focus_position, 0)
@@ -578,8 +571,8 @@ def test_grid_flow(self):
         self.assertRaises(ValueError, lambda: gf.set_focus('nonexistant'))
 
     def test_overlay(self):
-        s1 = urwid.SolidFill(u'1')
-        s2 = urwid.SolidFill(u'2')
+        s1 = urwid.SolidFill('1')
+        s2 = urwid.SolidFill('2')
         o = urwid.Overlay(s1, s2,
             'center', ('relative', 50), 'middle', ('relative', 50))
         self.assertEqual(o.focus, s1)
@@ -595,7 +588,7 @@ def test_overlay(self):
             'middle', None, 'relative', 50, None, 0, 0)))
 
     def test_frame(self):
-        s1 = urwid.SolidFill(u'1')
+        s1 = urwid.SolidFill('1')
 
         f = urwid.Frame(s1)
         self.assertEqual(f.focus, s1)
@@ -605,9 +598,9 @@ def test_frame(self):
         self.assertRaises(IndexError, lambda: setattr(f, 'focus_position',
             'header'))
 
-        t1 = urwid.Text(u'one')
-        t2 = urwid.Text(u'two')
-        t3 = urwid.Text(u'three')
+        t1 = urwid.Text('one')
+        t2 = urwid.Text('two')
+        t3 = urwid.Text('three')
         f = urwid.Frame(s1, t1, t2, 'header')
         self.assertEqual(f.focus, t1)
         self.assertEqual(f.focus_position, 'header')
@@ -631,12 +624,12 @@ def set2():
 
     def test_focus_path(self):
         # big tree of containers
-        t = urwid.Text(u'x')
-        e = urwid.Edit(u'?')
+        t = urwid.Text('x')
+        e = urwid.Edit('?')
         c = urwid.Columns([t, e, t, t])
         p = urwid.Pile([t, t, c, t])
         a = urwid.AttrMap(p, 'gets ignored')
-        s = urwid.SolidFill(u'/')
+        s = urwid.SolidFill('/')
         o = urwid.Overlay(e, s, 'center', 'pack', 'middle', 'pack')
         lb = urwid.ListBox(urwid.SimpleFocusListWalker([t, a, o, t]))
         lb.focus_position = 1
diff --git a/urwid/tests/test_decoration.py b/urwid/tests/test_decoration.py
index 8ab1acd29..7ce235c2d 100644
--- a/urwid/tests/test_decoration.py
+++ b/urwid/tests/test_decoration.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import unittest
 
 import urwid
@@ -7,8 +9,7 @@ class PaddingTest(unittest.TestCase):
     def ptest(self, desc, align, width, maxcol, left, right,min_width=None):
         p = urwid.Padding(None, align, width, min_width)
         l, r = p.padding_values((maxcol,),False)
-        assert (l,r)==(left,right), "%s expected %s but got %s"%(
-            desc, (left,right), (l,r))
+        assert (l,r)==(left,right), f"{desc} expected {left, right} but got {l, r}"
 
     def petest(self, desc, align, width):
         self.assertRaises(urwid.PaddingError, lambda:
@@ -77,13 +78,13 @@ def test_reduced_padding_cursor(self):
         # fixing this gets deep into things like Edit._shift_view_to_cursor
         # though, so this might not get fixed for a while
 
-        p = urwid.Padding(urwid.Edit(u'',u''), width='pack', left=4)
+        p = urwid.Padding(urwid.Edit('',''), width='pack', left=4)
         self.assertEqual(p.render((10,), True).cursor, None)
         self.assertEqual(p.get_cursor_coords((10,)), None)
         self.assertEqual(p.render((4,), True).cursor, None)
         self.assertEqual(p.get_cursor_coords((4,)), None)
 
-        p = urwid.Padding(urwid.Edit(u'',u''), width=('relative', 100), left=4)
+        p = urwid.Padding(urwid.Edit('',''), width=('relative', 100), left=4)
         self.assertEqual(p.render((10,), True).cursor, (4, 0))
         self.assertEqual(p.get_cursor_coords((10,)), (4, 0))
         self.assertEqual(p.render((4,), True).cursor, None)
@@ -95,8 +96,7 @@ def ftest(self, desc, valign, height, maxrow, top, bottom,
             min_height=None):
         f = urwid.Filler(None, valign, height, min_height)
         t, b = f.filler_values((20,maxrow), False)
-        assert (t,b)==(top,bottom), "%s expected %s but got %s"%(
-            desc, (top,bottom), (t,b))
+        assert (t,b)==(top,bottom), f"{desc} expected {top, bottom} but got {t, b}"
 
     def fetest(self, desc, valign, height):
         self.assertRaises(urwid.FillerError, lambda:
@@ -146,4 +146,4 @@ def test_values(self):
             10,1,1,8)
 
     def test_repr(self):
-        repr(urwid.Filler(urwid.Text(u'hai')))
+        repr(urwid.Filler(urwid.Text('hai')))
diff --git a/urwid/tests/test_doctests.py b/urwid/tests/test_doctests.py
index 9b7207092..cb0526ed7 100644
--- a/urwid/tests/test_doctests.py
+++ b/urwid/tests/test_doctests.py
@@ -1,9 +1,12 @@
-import unittest
+from __future__ import annotations
+
 import doctest
+import unittest
 
 import urwid
 import urwid.numedit
 
+
 def load_tests(loader, tests, ignore):
     module_doctests = [
         urwid.widget,
diff --git a/urwid/tests/test_escapes.py b/urwid/tests/test_escapes.py
index 7b1fe53f8..8d11c47e2 100644
--- a/urwid/tests/test_escapes.py
+++ b/urwid/tests/test_escapes.py
@@ -1,8 +1,9 @@
 #!/usr/bin/python
-# -*- coding: utf-8 -*-
 
 """ Tests covering escape sequences processing """
 
+from __future__ import annotations
+
 import unittest
 
 import urwid.escape
diff --git a/urwid/tests/test_event_loops.py b/urwid/tests/test_event_loops.py
index 49d1270eb..261a614e7 100644
--- a/urwid/tests/test_event_loops.py
+++ b/urwid/tests/test_event_loops.py
@@ -1,19 +1,19 @@
+from __future__ import annotations
+
 import os
 import unittest
-import platform
 
 import urwid
-from urwid.compat import PYTHON3
 
 
-class EventLoopTestMixin(object):
+class EventLoopTestMixin:
     def test_event_loop(self):
         rd, wr = os.pipe()
         evl = self.evl
         out = []
         def step1():
             out.append("writing")
-            os.write(wr, "hi".encode('ascii'))
+            os.write(wr, b"hi")
         def step2():
             out.append(os.read(rd, 2).decode('ascii'))
             raise urwid.ExitMainLoop
@@ -53,7 +53,7 @@ def test_run(self):
         evl = self.evl
         out = []
         rd, wr = os.pipe()
-        self.assertEqual(os.write(wr, "data".encode('ascii')), 4)
+        self.assertEqual(os.write(wr, b"data"), 4)
         def say_hello():
             out.append("hello")
         def say_waiting():
@@ -136,7 +136,7 @@ def test_run(self):
             evl = self.evl
             out = []
             rd, wr = os.pipe()
-            self.assertEqual(os.write(wr, "data".encode('ascii')), 4)
+            self.assertEqual(os.write(wr, b"data"), 4)
             def step2():
                 out.append(os.read(rd, 2).decode('ascii'))
             def say_hello():
diff --git a/urwid/tests/test_floatedit.py b/urwid/tests/test_floatedit.py
index 7ac5d2708..8db3ba844 100644
--- a/urwid/tests/test_floatedit.py
+++ b/urwid/tests/test_floatedit.py
@@ -1,8 +1,9 @@
 import unittest
-from urwid.numedit import FloatEdit
 from decimal import Decimal
 from itertools import product
 
+from urwid.numedit import FloatEdit
+
 
 class FloatEditNoPreservePrecicionTest(unittest.TestCase):
     def test(self):
diff --git a/urwid/tests/test_graphics.py b/urwid/tests/test_graphics.py
index 05808fcc3..f731208de 100644
--- a/urwid/tests/test_graphics.py
+++ b/urwid/tests/test_graphics.py
@@ -1,15 +1,16 @@
+from __future__ import annotations
+
 import unittest
 
-from urwid import graphics
-from urwid.compat import B
 import urwid
+from urwid import graphics
 
 
 class LineBoxTest(unittest.TestCase):
     def border(self, tl, t, tr, l, r, bl, b, br):
-        return [bytes().join([tl, t, tr]),
-                bytes().join([l, B(" "), r]),
-                bytes().join([bl, b, br]),]
+        return [b''.join([tl, t, tr]),
+                b''.join([l, b" ", r]),
+                b''.join([bl, b, br]),]
 
     def test_linebox_pack(self):
         # Bug #346 'pack' Padding does not run with LineBox
@@ -29,11 +30,11 @@ def test_linebox_border(self):
 
         # default
         self.assertEqual(l,
-            self.border(B("\xe2\x94\x8c"), B("\xe2\x94\x80"),
-                B("\xe2\x94\x90"), B("\xe2\x94\x82"), B("\xe2\x94\x82"),
-                B("\xe2\x94\x94"), B("\xe2\x94\x80"), B("\xe2\x94\x98")))
+            self.border(b"\xe2\x94\x8c", b"\xe2\x94\x80",
+                b"\xe2\x94\x90", b"\xe2\x94\x82", b"\xe2\x94\x82",
+                b"\xe2\x94\x94", b"\xe2\x94\x80", b"\xe2\x94\x98"))
 
-        nums = [B(str(n)) for n in range(8)]
+        nums = [str(n).encode('iso8859-1') for n in range(8)]
         b = dict(zip(["tlcorner", "tline", "trcorner", "lline", "rline",
             "blcorner", "bline", "brcorner"], nums))
         l = urwid.LineBox(t, **b).render((3,)).text
@@ -44,7 +45,7 @@ def test_linebox_border(self):
 class BarGraphTest(unittest.TestCase):
     def bgtest(self, desc, data, top, widths, maxrow, exp ):
         rval = graphics.calculate_bargraph_display(data,top,widths,maxrow)
-        assert rval == exp, "%s expected %r, got %r"%(desc,exp,rval)
+        assert rval == exp, f"{desc} expected {exp!r}, got {rval!r}"
 
     def test1(self):
         self.bgtest('simplest',[[0]],5,[1],1,
@@ -92,7 +93,7 @@ def sbgtest(self, desc, data, top, exp ):
                 None, {(1,0):'red/black', (2,1):'blue/red'})
         g.set_data( data, top )
         rval = g.calculate_display((5,3))
-        assert rval == exp, "%s expected %r, got %r"%(desc,exp,rval)
+        assert rval == exp, f"{desc} expected {exp!r}, got {rval!r}"
 
     def test1(self):
         self.sbgtest('simple', [[3]], 5,
diff --git a/urwid/tests/test_listbox.py b/urwid/tests/test_listbox.py
index 9d2eddbdc..846b17b43 100644
--- a/urwid/tests/test_listbox.py
+++ b/urwid/tests/test_listbox.py
@@ -1,8 +1,9 @@
+from __future__ import annotations
+
 import unittest
 
-from urwid.compat import B
-from urwid.tests.util import SelectableText
 import urwid
+from urwid.tests.util import SelectableText
 
 
 class ListBoxCalculateVisibleTest(unittest.TestCase):
@@ -22,8 +23,8 @@ def cvtest(self, desc, body, focus, offset_rows, inset_fraction,
             y += offset_inset
             cursor = x, y
 
-        assert offset_inset == exp_offset_inset, "%s got: %r expected: %r" %(desc,offset_inset,exp_offset_inset)
-        assert cursor == exp_cur, "%s (cursor) got: %r expected: %r" %(desc,cursor,exp_cur)
+        assert offset_inset == exp_offset_inset, f"{desc} got: {offset_inset!r} expected: {exp_offset_inset!r}"
+        assert cursor == exp_cur, f"{desc} (cursor) got: {cursor!r} expected: {exp_cur!r}"
 
     def test1_simple(self):
         T = urwid.Text
@@ -113,8 +114,8 @@ def cftest(self, desc, body, pos, offset_inset,
             if hasattr(focus_widget,'get_cursor_coords'):
                 cursor=focus_widget.get_cursor_coords((4,))
 
-        assert act == exp, "%s got: %s expected: %s" %(desc, act, exp)
-        assert cursor == exp_cur, "%s (cursor) got: %r expected: %r" %(desc,cursor,exp_cur)
+        assert act == exp, f"{desc} got: {act} expected: {exp}"
+        assert cursor == exp_cur, f"{desc} (cursor) got: {cursor!r} expected: {exp_cur!r}"
 
 
     def test1unselectable(self):
@@ -206,18 +207,18 @@ def test5set_focus_valign(self):
 
 class ListBoxRenderTest(unittest.TestCase):
     def ltest(self,desc,body,focus,offset_inset_rows,exp_text,exp_cur):
-        exp_text = [B(t) for t in exp_text]
+        exp_text = [t.encode('iso8859-1') for t in exp_text]
         lbox = urwid.ListBox(urwid.SimpleListWalker(body))
         lbox.body.set_focus( focus )
         lbox.shift_focus((4,10), offset_inset_rows )
         canvas = lbox.render( (4,5), focus=1 )
 
-        text = [bytes().join([t for at, cs, t in ln]) for ln in canvas.content()]
+        text = [b''.join([t for at, cs, t in ln]) for ln in canvas.content()]
 
         cursor = canvas.cursor
 
-        assert text == exp_text, "%s (text) got: %r expected: %r" %(desc,text,exp_text)
-        assert cursor == exp_cur, "%s (cursor) got: %r expected: %r" %(desc,cursor,exp_cur)
+        assert text == exp_text, f"{desc} (text) got: {text!r} expected: {exp_text!r}"
+        assert cursor == exp_cur, f"{desc} (cursor) got: {cursor!r} expected: {exp_cur!r}"
 
 
     def test1_simple(self):
@@ -292,11 +293,11 @@ def test3_shift(self):
     def test4_really_large_contents(self):
         T,E = urwid.Text, urwid.Edit
         self.ltest("really large edit",
-            [T(u"hello"*100)], 0, 0,
+            [T("hello"*100)], 0, 0,
             ["hell","ohel","lohe","lloh","ello"], None)
 
         self.ltest("really large edit",
-            [E(u"", u"hello"*100)], 0, 0,
+            [E("", "hello"*100)], 0, 0,
             ["hell","ohel","lohe","lloh","llo "], (3,4))
 
 
@@ -320,8 +321,8 @@ def ktest(self, desc, key, body, focus, offset_inset,
 
         exp = exp_focus, exp_offset_inset
         act = focus_pos, offset_inset
-        assert act == exp, "%s got: %r expected: %r" %(desc,act,exp)
-        assert cursor == exp_cur, "%s (cursor) got: %r expected: %r" %(desc,cursor,exp_cur)
+        assert act == exp, f"{desc} got: {act!r} expected: {exp!r}"
+        assert cursor == exp_cur, f"{desc} (cursor) got: {cursor!r} expected: {exp_cur!r}"
         return ret_key,lbox
 
 
@@ -768,7 +769,7 @@ def test_listbox_pile(self):
 
     def test_listbox_text_pile_page_down(self):
         lb = urwid.ListBox(urwid.SimpleListWalker(
-            [urwid.Text(u'above'), urwid.Pile([])]))
+            [urwid.Text('above'), urwid.Pile([])]))
         lb.keypress((40,10), 'page down')
         self.assertEqual(lb.get_focus()[1], 0)
         lb.keypress((40,10), 'page down') # second one caused ListBox failure
@@ -776,7 +777,7 @@ def test_listbox_text_pile_page_down(self):
 
     def test_listbox_text_pile_page_up(self):
         lb = urwid.ListBox(urwid.SimpleListWalker(
-            [urwid.Pile([]), urwid.Text(u'below')]))
+            [urwid.Pile([]), urwid.Text('below')]))
         lb.set_focus(1)
         lb.keypress((40,10), 'page up')
         self.assertEqual(lb.get_focus()[1], 1)
@@ -786,7 +787,7 @@ def test_listbox_text_pile_page_up(self):
     def test_listbox_text_pile_down(self):
         sp = urwid.Pile([])
         sp.selectable = lambda: True # abuse our Pile
-        lb = urwid.ListBox(urwid.SimpleListWalker([urwid.Text(u'above'), sp]))
+        lb = urwid.ListBox(urwid.SimpleListWalker([urwid.Text('above'), sp]))
         lb.keypress((40,10), 'down')
         self.assertEqual(lb.get_focus()[1], 0)
         lb.keypress((40,10), 'down')
@@ -795,7 +796,7 @@ def test_listbox_text_pile_down(self):
     def test_listbox_text_pile_up(self):
         sp = urwid.Pile([])
         sp.selectable = lambda: True # abuse our Pile
-        lb = urwid.ListBox(urwid.SimpleListWalker([sp, urwid.Text(u'below')]))
+        lb = urwid.ListBox(urwid.SimpleListWalker([sp, urwid.Text('below')]))
         lb.set_focus(1)
         lb.keypress((40,10), 'up')
         self.assertEqual(lb.get_focus()[1], 1)
diff --git a/urwid/tests/test_signals.py b/urwid/tests/test_signals.py
index 22db34e75..1842bd1d3 100644
--- a/urwid/tests/test_signals.py
+++ b/urwid/tests/test_signals.py
@@ -1,11 +1,20 @@
 import unittest
+
 try:
     from unittest.mock import Mock
 except ImportError:
     # Python2, rely on PyPi
     from mock import Mock
 
-from urwid import connect_signal, disconnect_signal, register_signal, emit_signal, Signals, Edit
+from urwid import (
+    Edit,
+    Signals,
+    connect_signal,
+    disconnect_signal,
+    emit_signal,
+    register_signal,
+)
+
 
 class SiglnalsTest(unittest.TestCase):
 
diff --git a/urwid/tests/test_str_util.py b/urwid/tests/test_str_util.py
index f7ad1d9d2..2fec29e5b 100644
--- a/urwid/tests/test_str_util.py
+++ b/urwid/tests/test_str_util.py
@@ -1,15 +1,16 @@
+from __future__ import annotations
+
 import unittest
 
-from urwid.compat import B
 from urwid.escape import str_util
 
 
 class DecodeOneTest(unittest.TestCase):
     def gwt(self, ch, exp_ord, exp_pos):
-        ch = B(ch)
+        ch = ch.encode('iso8859-1')
         o, pos = str_util.decode_one(ch,0)
-        assert o==exp_ord, " got:%r expected:%r" % (o, exp_ord)
-        assert pos==exp_pos, " got:%r expected:%r" % (pos, exp_pos)
+        assert o==exp_ord, f" got:{o!r} expected:{exp_ord!r}"
+        assert pos==exp_pos, f" got:{pos!r} expected:{exp_pos!r}"
 
     def test1byte(self):
         self.gwt("ab", ord("a"), 1)
diff --git a/urwid/tests/test_text_layout.py b/urwid/tests/test_text_layout.py
index b446319dd..77bd27d40 100644
--- a/urwid/tests/test_text_layout.py
+++ b/urwid/tests/test_text_layout.py
@@ -1,14 +1,15 @@
+from __future__ import annotations
+
 import unittest
 
-from urwid import text_layout
-from urwid.compat import B
 import urwid
+from urwid import text_layout
 
 
-class CalcBreaksTest(object):
+class CalcBreaksTest:
     def cbtest(self, width, exp):
         result = text_layout.default_layout.calculate_text_segments(
-            B(self.text), width, self.mode )
+            self.text.encode('iso8859-1'), width, self.mode )
         assert len(result) == len(exp), repr((result, exp))
         for l,e in zip(result, exp):
             end = l[-1][-1]
@@ -97,11 +98,11 @@ def test(self):
         urwid.set_encoding("euc-jp")
         self.assertRaises(text_layout.CanNotDisplayText,
             text_layout.default_layout.calculate_text_segments,
-            B('\xA1\xA1'), 1, 'space' )
+            b'\xA1\xA1', 1, 'space' )
         urwid.set_encoding("utf-8")
         self.assertRaises(text_layout.CanNotDisplayText,
             text_layout.default_layout.calculate_text_segments,
-            B('\xe9\xa2\x96'), 1, 'space' )
+            b'\xe9\xa2\x96', 1, 'space' )
 
 
 class SubsegTest(unittest.TestCase):
@@ -109,10 +110,10 @@ def setUp(self):
         urwid.set_encoding("euc-jp")
 
     def st(self, seg, text, start, end, exp):
-        text = B(text)
+        text = text.encode('iso8859-1')
         s = urwid.LayoutSegment(seg)
         result = s.subseg( text, start, end )
-        assert result == exp, "Expected %r, got %r"%(exp,result)
+        assert result == exp, f"Expected {exp!r}, got {result!r}"
 
     def test1_padding(self):
         self.st( (10, None), "", 0, 8,    [(8, None)] )
@@ -121,18 +122,18 @@ def test1_padding(self):
         self.st( (10, 0), "", 0, 20,     [(10, 0)] )
 
     def test2_text(self):
-        self.st( (10, 0, B("1234567890")), "", 0, 8,  [(8,0,B("12345678"))] )
-        self.st( (10, 0, B("1234567890")), "", 2, 10, [(8,0,B("34567890"))] )
-        self.st( (10, 0, B("12\xA1\xA156\xA1\xA190")), "", 2, 8,
-            [(6, 0, B("\xA1\xA156\xA1\xA1"))] )
-        self.st( (10, 0, B("12\xA1\xA156\xA1\xA190")), "", 3, 8,
-            [(5, 0, B(" 56\xA1\xA1"))] )
-        self.st( (10, 0, B("12\xA1\xA156\xA1\xA190")), "", 2, 7,
-            [(5, 0, B("\xA1\xA156 "))] )
-        self.st( (10, 0, B("12\xA1\xA156\xA1\xA190")), "", 3, 7,
-            [(4, 0, B(" 56 "))] )
-        self.st( (10, 0, B("12\xA1\xA156\xA1\xA190")), "", 0, 20,
-            [(10, 0, B("12\xA1\xA156\xA1\xA190"))] )
+        self.st( (10, 0, b"1234567890"), "", 0, 8,  [(8,0,b"12345678")] )
+        self.st( (10, 0, b"1234567890"), "", 2, 10, [(8,0,b"34567890")] )
+        self.st( (10, 0, b"12\xA1\xA156\xA1\xA190"), "", 2, 8,
+            [(6, 0, b"\xA1\xA156\xA1\xA1")] )
+        self.st( (10, 0, b"12\xA1\xA156\xA1\xA190"), "", 3, 8,
+            [(5, 0, b" 56\xA1\xA1")] )
+        self.st( (10, 0, b"12\xA1\xA156\xA1\xA190"), "", 2, 7,
+            [(5, 0, b"\xA1\xA156 ")] )
+        self.st( (10, 0, b"12\xA1\xA156\xA1\xA190"), "", 3, 7,
+            [(4, 0, b" 56 ")] )
+        self.st( (10, 0, b"12\xA1\xA156\xA1\xA190"), "", 0, 20,
+            [(10, 0, b"12\xA1\xA156\xA1\xA190")] )
 
     def test3_range(self):
         t = "1234567890"
@@ -149,7 +150,7 @@ def test3_range(self):
         self.st( (6, 2, 8), t, 1, 5,     [(1, 3), (2, 4, 6), (1, 6)] )
 
 
-class CalcTranslateTest(object):
+class CalcTranslateTest:
     def setUp(self):
         urwid.set_encoding("utf-8")
 
@@ -227,7 +228,7 @@ class CalcTranslateWordTest3(CalcTranslateTest, unittest.TestCase):
     def setUp(self):
         urwid.set_encoding('utf-8')
 
-    text = B('\xe6\x9b\xbf\xe6\xb4\xbc\n\xe6\xb8\x8e\xe6\xba\x8f\xe6\xbd\xba')
+    text = b'\xe6\x9b\xbf\xe6\xb4\xbc\n\xe6\xb8\x8e\xe6\xba\x8f\xe6\xbd\xba'
     width = 10
     mode = 'space'
     result_left = [
@@ -292,7 +293,7 @@ class CalcTranslateClipTest(CalcTranslateTest, unittest.TestCase):
         [(14, 36, 50), (0, 50)]]
 
 class CalcTranslateCantDisplayTest(CalcTranslateTest, unittest.TestCase):
-    text = B('Hello\xe9\xa2\x96')
+    text = b'Hello\xe9\xa2\x96'
     mode = 'space'
     width = 1
     result_left = [[]]
@@ -314,8 +315,7 @@ def setUp(self):
     def tests(self):
         for x,y, expected in self.mytests:
             got = text_layout.calc_pos( self.text, self.trans, x, y )
-            assert got == expected, "%r got:%r expected:%r" % ((x, y), got,
-                                                               expected)
+            assert got == expected, f"{x, y!r} got:{got!r} expected:{expected!r}"
 
 
 class Pos2CoordsTest(unittest.TestCase):
@@ -339,4 +339,4 @@ def test(self):
         for t, answer in self.mytests:
             for pos,a in zip(self.pos_list,answer) :
                 r = text_layout.calc_coords( self.text, t, pos)
-                assert r==a, "%r got: %r expected: %r"%(t,r,a)
+                assert r==a, f"{t!r} got: {r!r} expected: {a!r}"
diff --git a/urwid/tests/test_util.py b/urwid/tests/test_util.py
index 2aae07532..abe7aa5e7 100644
--- a/urwid/tests/test_util.py
+++ b/urwid/tests/test_util.py
@@ -1,17 +1,17 @@
-# -*- coding: utf-8 -*-
-import unittest
+from __future__ import annotations
+
 import locale
+import unittest
 
 import urwid
 from urwid import util
-from urwid.compat import B
 
 
 class CalcWidthTest(unittest.TestCase):
     def wtest(self, desc, s, exp):
-        s = B(s)
+        s = s.encode('iso8859-1')
         result = util.calc_width( s, 0, len(s))
-        assert result==exp, "%s got:%r expected:%r" % (desc, result, exp)
+        assert result==exp, f"{desc} got:{result!r} expected:{exp!r}"
 
     def test1(self):
         util.set_encoding("utf-8")
@@ -30,23 +30,22 @@ def test2(self):
 
 class ConvertDecSpecialTest(unittest.TestCase):
     def ctest(self, desc, s, exp, expcs):
-        exp = B(exp)
+        exp = exp.encode('iso8859-1')
         util.set_encoding('ascii')
         c = urwid.Text(s).render((5,))
         result = c._text[0]
-        assert result==exp, "%s got:%r expected:%r" % (desc, result, exp)
+        assert result==exp, f"{desc} got:{result!r} expected:{exp!r}"
         resultcs = c._cs[0]
-        assert resultcs==expcs, "%s got:%r expected:%r" % (desc,
-                                                           resultcs, expcs)
+        assert resultcs==expcs, f"{desc} got:{resultcs!r} expected:{expcs!r}"
 
     def test1(self):
-        self.ctest("no conversion", u"hello", "hello", [(None,5)])
-        self.ctest("only special", u"£££££", "}}}}}", [("0",5)])
-        self.ctest("mix left", u"££abc", "}}abc", [("0",2),(None,3)])
-        self.ctest("mix right", u"abc££", "abc}}", [(None,3),("0",2)])
-        self.ctest("mix inner", u"a££bc", "a}}bc",
+        self.ctest("no conversion", "hello", "hello", [(None,5)])
+        self.ctest("only special", "£££££", "}}}}}", [("0",5)])
+        self.ctest("mix left", "££abc", "}}abc", [("0",2),(None,3)])
+        self.ctest("mix right", "abc££", "abc}}", [(None,3),("0",2)])
+        self.ctest("mix inner", "a££bc", "a}}bc",
             [(None,1),("0",2),(None,2)] )
-        self.ctest("mix well", u"£a£b£", "}a}b}",
+        self.ctest("mix well", "£a£b£", "}a}b}",
             [("0",1),(None,1),("0",1),(None,1),("0",1)] )
 
 
@@ -55,9 +54,8 @@ def setUp(self):
         urwid.set_encoding("euc-jp")
 
     def wtest(self, s, ls, pos, expected, desc):
-        result = util.within_double_byte(B(s), ls, pos)
-        assert result==expected, "%s got:%r expected: %r" % (desc,
-                                                             result, expected)
+        result = util.within_double_byte(s.encode('iso8859-1'), ls, pos)
+        assert result==expected, f"{desc} got:{result!r} expected: {expected!r}"
     def test1(self):
         self.wtest("mnopqr",0,2,0,'simple no high bytes')
         self.wtest("mn\xA1\xA1qr",0,2,1,'simple 1st half')
@@ -91,11 +89,10 @@ def test3(self):
 
 class CalcTextPosTest(unittest.TestCase):
     def ctptest(self, text, tests):
-        text = B(text)
+        text = text.encode('iso8859-1')
         for s,e,p, expected in tests:
             got = util.calc_text_pos( text, s, e, p )
-            assert got == expected, "%r got:%r expected:%r" % ((s,e,p),
-                                                               got, expected)
+            assert got == expected, f"{s, e, p!r} got:{got!r} expected:{expected!r}"
 
     def test1(self):
         text = "hello world out there"
@@ -160,15 +157,15 @@ class TagMarkupTest(unittest.TestCase):
         (["mix",[" it",('high',[" up",('ital'," a")])]," little"],
             "mix it up a little",
             [(None, 6), ('high', 3), ('ital', 2)]),
-        ([u"££", u"x££"], u"££x££", []),
-        ([B("\xc2\x80"), B("\xc2\x80")], B("\xc2\x80\xc2\x80"), []),
+        (["££", "x££"], "££x££", []),
+        ([b"\xc2\x80", b"\xc2\x80"], b"\xc2\x80\xc2\x80", []),
         ]
 
     def test(self):
         for input, text, attr in self.mytests:
             restext,resattr = urwid.decompose_tagmarkup( input )
-            assert restext == text, "got: %r expected: %r" % (restext, text)
-            assert resattr == attr, "got: %r expected: %r" % (resattr, attr)
+            assert restext == text, f"got: {restext!r} expected: {text!r}"
+            assert resattr == attr, f"got: {resattr!r} expected: {attr!r}"
 
     def test_bad_tuple(self):
         self.assertRaises(urwid.TagMarkupException, lambda:
diff --git a/urwid/tests/test_vterm.py b/urwid/tests/test_vterm.py
index cefa0891f..6ca6fc968 100644
--- a/urwid/tests/test_vterm.py
+++ b/urwid/tests/test_vterm.py
@@ -16,23 +16,23 @@
 #    License along with this library; if not, write to the Free Software
 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 #
-# Urwid web site: http://excess.org/urwid/
+# Urwid web site: https://urwid.org/
+
+from __future__ import annotations
 
 import errno
 import os
 import sys
 import unittest
-
 from itertools import dropwhile
 
-from urwid.listbox import ListBox
+from urwid import signals, vterm
 from urwid.decoration import BoxAdapter
-from urwid import vterm
-from urwid import signals
-from urwid.compat import B
+from urwid.listbox import ListBox
+
 
-class DummyCommand(object):
-    QUITSTRING = B('|||quit|||')
+class DummyCommand:
+    QUITSTRING = b'|||quit|||'
 
     def __init__(self):
         self.reader, self.writer = os.pipe()
@@ -40,7 +40,7 @@ def __init__(self):
     def __call__(self):
         # reset
         stdout = getattr(sys.stdout, 'buffer', sys.stdout)
-        stdout.write(B('\x1bc'))
+        stdout.write(b'\x1bc')
 
         while True:
             data = self.read(1024)
@@ -98,8 +98,8 @@ def resize(self, width, height, soft=False):
             self.term.render(self.termsize, focus=False)
 
     def write(self, data):
-        data = B(data)
-        self.command.write(data.replace(B('\e'), B('\x1b')))
+        data = data.encode('iso8859-1')
+        self.command.write(data.replace(br'\e', b'\x1b'))
 
     def flush(self):
         self.write(chr(0x7f))
@@ -108,7 +108,7 @@ def read(self, raw=False, focus=False):
         self.term.wait_and_feed()
         rendered = self.term.render(self.termsize, focus=focus)
         if raw:
-            is_empty = lambda c: c == (None, None, B(' '))
+            is_empty = lambda c: c == (None, None, b' ')
             content = list(rendered.content())
             lines = [list(dropwhile(is_empty, reversed(line)))
                      for line in content]
@@ -116,11 +116,11 @@ def read(self, raw=False, focus=False):
         else:
             content = rendered.text
             lines = [line.rstrip() for line in content]
-            return B('\n').join(lines).rstrip()
+            return b'\n'.join(lines).rstrip()
 
     def expect(self, what, desc=None, raw=False, focus=False):
         if not isinstance(what, list):
-            what = B(what)
+            what = what.encode('iso8859-1')
         got = self.read(raw=raw, focus=focus)
         if desc is None:
             desc = ''
@@ -138,7 +138,7 @@ def test_linefeed(self):
         self.expect('hello\nworld')
 
     def test_linefeed2(self):
-        self.write('aa\b\b\eDbb')
+        self.write('aa\b\b\\eDbb')
         self.expect('aa\nbb')
 
     def test_carriage_return(self):
@@ -146,11 +146,11 @@ def test_carriage_return(self):
         self.expect('world')
 
     def test_insertlines(self):
-        self.write('\e[0;0flast\e[0;0f\e[10L\e[0;0ffirst\nsecond\n\e[11D')
+        self.write('\\e[0;0flast\\e[0;0f\\e[10L\\e[0;0ffirst\nsecond\n\\e[11D')
         self.expect('first\nsecond\n\n\n\n\n\n\n\n\nlast')
 
     def test_deletelines(self):
-        self.write('1\n2\n3\n4\e[2;1f\e[2M')
+        self.write('1\n2\n3\n4\\e[2;1f\\e[2M')
         self.expect('1\n4')
 
     def test_nul(self):
@@ -158,21 +158,21 @@ def test_nul(self):
         self.expect('ab')
 
     def test_movement(self):
-        self.write('\e[10;20H11\e[10;0f\e[20C\e[K')
+        self.write(r'\e[10;20H11\e[10;0f\e[20C\e[K')
         self.expect('\n' * 9 + ' ' * 19 + '1')
-        self.write('\e[A\e[B\e[C\e[D\b\e[K')
+        self.write('\\e[A\\e[B\\e[C\\e[D\b\\e[K')
         self.expect('')
-        self.write('\e[50A2')
+        self.write(r'\e[50A2')
         self.expect(' ' * 19 + '2')
-        self.write('\b\e[K\e[50B3')
+        self.write('\b\\e[K\\e[50B3')
         self.expect('\n' * 23 + ' ' * 19 + '3')
-        self.write('\b\e[K' + '\eM' * 30 + '\e[100C4')
+        self.write('\b\\e[K' + r'\eM' * 30 + r'\e[100C4')
         self.expect(' ' * 79 + '4')
-        self.write('\e[100D\e[K5')
+        self.write(r'\e[100D\e[K5')
         self.expect('5')
 
     def edgewall(self):
-        edgewall = '1-\e[1;%(x)df-2\e[%(y)d;1f3-\e[%(y)d;%(x)df-4\x0d'
+        edgewall = '1-\\e[1;%(x)df-2\\e[%(y)d;1f3-\\e[%(y)d;%(x)df-4\x0d'
         self.write(edgewall % {'x': self.termsize[0] - 1,
                                'y': self.termsize[1] - 1})
 
@@ -195,7 +195,7 @@ def test_vertical_resize(self):
                          + '3-' + ' ' * 76 + '-4')
         for y in range(23, 1, -1):
             self.resize(80, y, soft=True)
-            self.write('\e[%df\e[J3-\e[%d;%df-4' % (y, y, 79))
+            self.write(r'\e[%df\e[J3-\e[%d;%df-4' % (y, y, 79))
             desc = "try to rescale to 80x%d." % y
             self.expect('\n' * (y - 2) + '3-' + ' ' * 76 + '-4', desc)
         self.resize(80, 24, soft=True)
@@ -204,8 +204,8 @@ def test_vertical_resize(self):
                          + '3-' + ' ' * 76 + '-4')
 
     def write_movements(self, arg):
-        fmt = 'XXX\n\e[faaa\e[Bccc\e[Addd\e[Bfff\e[Cbbb\e[A\e[Deee'
-        self.write(fmt.replace('\e[', '\e['+arg))
+        fmt = 'XXX\n\\e[faaa\\e[Bccc\\e[Addd\\e[Bfff\\e[Cbbb\\e[A\\e[Deee'
+        self.write(fmt.replace(r'\e[', r'\e['+arg))
 
     def test_defargs(self):
         self.write_movements('')
@@ -216,82 +216,82 @@ def test_nullargs(self):
         self.expect('aaa   ddd      eee\n   ccc   fff bbb')
 
     def test_erase_line(self):
-        self.write('1234567890\e[5D\e[K\n1234567890\e[5D\e[1K\naaaaaaaaaaaaaaa\e[2Ka')
+        self.write('1234567890\\e[5D\\e[K\n1234567890\\e[5D\\e[1K\naaaaaaaaaaaaaaa\\e[2Ka')
         self.expect('12345\n      7890\n               a')
 
     def test_erase_display(self):
-        self.write('1234567890\e[5D\e[Ja')
+        self.write(r'1234567890\e[5D\e[Ja')
         self.expect('12345a')
-        self.write('98765\e[8D\e[1Jx')
+        self.write(r'98765\e[8D\e[1Jx')
         self.expect('   x5a98765')
 
     def test_scrolling_region_simple(self):
-        self.write('\e[10;20r\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\e[faa')
+        self.write('\\e[10;20r\\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\\e[faa')
         self.expect('aa' + '\n' * 9 + '2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12')
 
     def test_scrolling_region_reverse(self):
-        self.write('\e[2J\e[1;2r\e[5Baaa\r\eM\eM\eMbbb\nXXX')
+        self.write('\\e[2J\\e[1;2r\\e[5Baaa\r\\eM\\eM\\eMbbb\nXXX')
         self.expect('\n\nbbb\nXXX\n\naaa')
 
     def test_scrolling_region_move(self):
-        self.write('\e[10;20r\e[2J\e[10Bfoo\rbar\rblah\rmooh\r\e[10Aone\r\eM\eMtwo\r\eM\eMthree\r\eM\eMa')
+        self.write('\\e[10;20r\\e[2J\\e[10Bfoo\rbar\rblah\rmooh\r\\e[10Aone\r\\eM\\eMtwo\r\\eM\\eMthree\r\\eM\\eMa')
         self.expect('ahree\n\n\n\n\n\n\n\n\n\nmooh')
 
     def test_scrolling_twice(self):
-        self.write('\e[?6h\e[10;20r\e[2;5rtest')
+        self.write(r'\e[?6h\e[10;20r\e[2;5rtest')
         self.expect('\ntest')
 
     def test_cursor_scrolling_region(self):
-        self.write('\e[?6h\e[10;20r\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\e[faa')
+        self.write('\\e[?6h\\e[10;20r\\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\\e[faa')
         self.expect('\n' * 9 + 'aa\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12')
 
     def test_scrolling_region_simple_with_focus(self):
-        self.write('\e[10;20r\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\e[faa')
+        self.write('\\e[10;20r\\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\\e[faa')
         self.expect('aa' + '\n' * 9 + '2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12', focus=True)
 
     def test_scrolling_region_reverse_with_focus(self):
-        self.write('\e[2J\e[1;2r\e[5Baaa\r\eM\eM\eMbbb\nXXX')
+        self.write('\\e[2J\\e[1;2r\\e[5Baaa\r\\eM\\eM\\eMbbb\nXXX')
         self.expect('\n\nbbb\nXXX\n\naaa', focus=True)
 
     def test_scrolling_region_move_with_focus(self):
-        self.write('\e[10;20r\e[2J\e[10Bfoo\rbar\rblah\rmooh\r\e[10Aone\r\eM\eMtwo\r\eM\eMthree\r\eM\eMa')
+        self.write('\\e[10;20r\\e[2J\\e[10Bfoo\rbar\rblah\rmooh\r\\e[10Aone\r\\eM\\eMtwo\r\\eM\\eMthree\r\\eM\\eMa')
         self.expect('ahree\n\n\n\n\n\n\n\n\n\nmooh', focus=True)
 
     def test_scrolling_twice_with_focus(self):
-        self.write('\e[?6h\e[10;20r\e[2;5rtest')
+        self.write(r'\e[?6h\e[10;20r\e[2;5rtest')
         self.expect('\ntest', focus=True)
 
     def test_cursor_scrolling_region_with_focus(self):
-        self.write('\e[?6h\e[10;20r\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\e[faa')
+        self.write('\\e[?6h\\e[10;20r\\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\\e[faa')
         self.expect('\n' * 9 + 'aa\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12', focus=True)
 
     def test_relative_region_jump(self):
-        self.write('\e[21H---\e[10;20r\e[?6h\e[18Htest')
+        self.write(r'\e[21H---\e[10;20r\e[?6h\e[18Htest')
         self.expect('\n' * 19 + 'test\n---')
 
     def test_set_multiple_modes(self):
-        self.write('\e[?6;5htest')
+        self.write(r'\e[?6;5htest')
         self.expect('test')
         self.assertTrue(self.term.term_modes.constrain_scrolling)
         self.assertTrue(self.term.term_modes.reverse_video)
-        self.write('\e[?6;5l')
+        self.write(r'\e[?6;5l')
         self.expect('test')
         self.assertFalse(self.term.term_modes.constrain_scrolling)
         self.assertFalse(self.term.term_modes.reverse_video)
 
     def test_wrap_simple(self):
-        self.write('\e[?7h\e[1;%dHtt' % self.term.width)
+        self.write(r'\e[?7h\e[1;%dHtt' % self.term.width)
         self.expect(' ' * (self.term.width - 1) + 't\nt')
 
     def test_wrap_backspace_tab(self):
-        self.write('\e[?7h\e[1;%dHt\b\b\t\ta' % self.term.width)
+        self.write('\\e[?7h\\e[1;%dHt\b\b\t\ta' % self.term.width)
         self.expect(' ' * (self.term.width - 1) + 'a')
 
     def test_cursor_visibility(self):
-        self.write('\e[?25linvisible')
+        self.write(r'\e[?25linvisible')
         self.expect('invisible', focus=True)
         self.assertEqual(self.term.term.cursor, None)
-        self.write('\rvisible\e[?25h\e[K')
+        self.write('\rvisible\\e[?25h\\e[K')
         self.expect('visible', focus=True)
         self.assertNotEqual(self.term.term.cursor, None)
 
@@ -305,41 +305,41 @@ def test_get_utf8_len(self):
 
     def test_encoding_unicode(self):
         vterm.util._target_encoding = 'utf-8'
-        self.write('\e%G\xe2\x80\x94')
+        self.write('\\e%G\xe2\x80\x94')
         self.expect('\xe2\x80\x94')
 
     def test_encoding_unicode_ascii(self):
         vterm.util._target_encoding = 'ascii'
-        self.write('\e%G\xe2\x80\x94')
+        self.write('\\e%G\xe2\x80\x94')
         self.expect('?')
 
     def test_encoding_wrong_unicode(self):
         vterm.util._target_encoding = 'utf-8'
-        self.write('\e%G\xc0\x99')
+        self.write('\\e%G\xc0\x99')
         self.expect('')
 
     def test_encoding_vt100_graphics(self):
         vterm.util._target_encoding = 'ascii'
-        self.write('\e)0\e(0\x0fg\x0eg\e)Bn\e)0g\e)B\e(B\x0fn')
+        self.write('\\e)0\\e(0\x0fg\x0eg\\e)Bn\\e)0g\\e)B\\e(B\x0fn')
         self.expect([[
-            (None, '0', B('g')), (None, '0', B('g')),
-            (None, None, B('n')), (None, '0', B('g')),
-            (None, None, B('n'))
+            (None, '0', b'g'), (None, '0', b'g'),
+            (None, None, b'n'), (None, '0', b'g'),
+            (None, None, b'n')
         ]], raw=True)
 
     def test_ibmpc_mapping(self):
         vterm.util._target_encoding = 'ascii'
 
-        self.write('\e[11m\x18\e[10m\x18')
-        self.expect([[(None, 'U', B('\x18'))]], raw=True)
+        self.write('\\e[11m\x18\\e[10m\x18')
+        self.expect([[(None, 'U', b'\x18')]], raw=True)
 
-        self.write('\ec\e)U\x0e\x18\x0f\e[3h\x18\e[3l\x18')
-        self.expect([[(None, None, B('\x18'))]], raw=True)
+        self.write('\\ec\\e)U\x0e\x18\x0f\\e[3h\x18\\e[3l\x18')
+        self.expect([[(None, None, b'\x18')]], raw=True)
 
-        self.write('\ec\e[11m\xdb\x18\e[10m\xdb')
+        self.write('\\ec\\e[11m\xdb\x18\\e[10m\xdb')
         self.expect([[
-            (None, 'U', B('\xdb')), (None, 'U', B('\x18')),
-            (None, None, B('\xdb'))
+            (None, 'U', b'\xdb'), (None, 'U', b'\x18'),
+            (None, None, b'\xdb')
         ]], raw=True)
 
     def test_set_title(self):
@@ -349,20 +349,20 @@ def _change_title(widget, title):
             self._the_title = title
 
         self.connect_signal('title')
-        self.write('\e]666parsed right?\e\\te\e]0;test title\007st1')
+        self.write('\\e]666parsed right?\\e\\te\\e]0;test title\007st1')
         self.expect('test1')
-        self.expect_signal(B('test title'))
-        self.write('\e]3;stupid title\e\\\e[0G\e[2Ktest2')
+        self.expect_signal(b'test title')
+        self.write('\\e]3;stupid title\\e\\\\e[0G\\e[2Ktest2')
         self.expect('test2')
-        self.expect_signal(B('stupid title'))
+        self.expect_signal(b'stupid title')
         self.disconnect_signal('title')
 
     def test_set_leds(self):
         self.connect_signal('leds')
-        self.write('\e[0qtest1')
+        self.write(r'\e[0qtest1')
         self.expect('test1')
         self.expect_signal('clear')
-        self.write('\e[3q\e[H\e[Ktest2')
+        self.write(r'\e[3q\e[H\e[Ktest2')
         self.expect('test2')
         self.expect_signal('caps_lock')
         self.disconnect_signal('leds')
diff --git a/urwid/tests/test_widget.py b/urwid/tests/test_widget.py
index 3f28bc18d..4ea2944ce 100644
--- a/urwid/tests/test_widget.py
+++ b/urwid/tests/test_widget.py
@@ -1,7 +1,7 @@
-# -*- coding: utf-8 -*-
+from __future__ import annotations
+
 import unittest
 
-from urwid.compat import B
 import urwid
 
 
@@ -10,48 +10,46 @@ def setUp(self):
         self.t = urwid.Text("I walk the\ncity in the night")
 
     def test1_wrap(self):
-        expected = [B(t) for t in ("I walk the","city in   ","the night ")]
+        expected = [t.encode('iso8859-1') for t in ("I walk the","city in   ","the night ")]
         got = self.t.render((10,))._text
-        assert got == expected, "got: %r expected: %r" % (got, expected)
+        assert got == expected, f"got: {got!r} expected: {expected!r}"
 
     def test2_left(self):
         self.t.set_align_mode('left')
-        expected = [B(t) for t in ("I walk the        ","city in the night ")]
+        expected = [t.encode('iso8859-1') for t in ("I walk the        ","city in the night ")]
         got = self.t.render((18,))._text
-        assert got == expected, "got: %r expected: %r" % (got, expected)
+        assert got == expected, f"got: {got!r} expected: {expected!r}"
 
     def test3_right(self):
         self.t.set_align_mode('right')
-        expected = [B(t) for t in ("        I walk the"," city in the night")]
+        expected = [t.encode('iso8859-1') for t in ("        I walk the"," city in the night")]
         got = self.t.render((18,))._text
-        assert got == expected, "got: %r expected: %r" % (got, expected)
+        assert got == expected, f"got: {got!r} expected: {expected!r}"
 
     def test4_center(self):
         self.t.set_align_mode('center')
-        expected = [B(t) for t in ("    I walk the    "," city in the night")]
+        expected = [t.encode('iso8859-1') for t in ("    I walk the    "," city in the night")]
         got = self.t.render((18,))._text
-        assert got == expected, "got: %r expected: %r" % (got, expected)
+        assert got == expected, f"got: {got!r} expected: {expected!r}"
 
     def test5_encode_error(self):
         urwid.set_encoding("ascii")
-        expected = [B("?  ")]
-        got = urwid.Text(u'û').render((3,))._text
-        assert got == expected, "got: %r expected: %r" % (got, expected)
+        expected = [b"?  "]
+        got = urwid.Text('û').render((3,))._text
+        assert got == expected, f"got: {got!r} expected: {expected!r}"
 
 
 class EditTest(unittest.TestCase):
     def setUp(self):
-        self.t1 = urwid.Edit(B(""),"blah blah")
-        self.t2 = urwid.Edit(B("stuff:"), "blah blah")
-        self.t3 = urwid.Edit(B("junk:\n"),"blah blah\n\nbloo",1)
-        self.t4 = urwid.Edit(u"better:")
+        self.t1 = urwid.Edit(b"","blah blah")
+        self.t2 = urwid.Edit(b"stuff:", "blah blah")
+        self.t3 = urwid.Edit(b"junk:\n","blah blah\n\nbloo",1)
+        self.t4 = urwid.Edit("better:")
 
     def ktest(self, e, key, expected, pos, desc):
         got= e.keypress((12,),key)
-        assert got == expected, "%s.  got: %r expected:%r" % (desc, got,
-                                                              expected)
-        assert e.edit_pos == pos, "%s. pos: %r expected pos: %r" % (
-            desc, e.edit_pos, pos)
+        assert got == expected, f"{desc}.  got: {got!r} expected:{expected!r}"
+        assert e.edit_pos == pos, f"{desc}. pos: {e.edit_pos!r} expected pos: {pos!r}"
 
     def test1_left(self):
         self.t1.set_edit_pos(0)
@@ -91,22 +89,21 @@ def test4_down(self):
     def test_utf8_input(self):
         urwid.set_encoding("utf-8")
         self.t1.set_edit_text('')
-        self.t1.keypress((12,), u'û')
-        self.assertEqual(self.t1.edit_text, u'û'.encode('utf-8'))
-        self.t4.keypress((12,), u'û')
-        self.assertEqual(self.t4.edit_text, u'û')
+        self.t1.keypress((12,), 'û')
+        self.assertEqual(self.t1.edit_text, 'û'.encode())
+        self.t4.keypress((12,), 'û')
+        self.assertEqual(self.t4.edit_text, 'û')
 
 
 class EditRenderTest(unittest.TestCase):
     def rtest(self, w, expected_text, expected_cursor):
-        expected_text = [B(t) for t in expected_text]
+        expected_text = [t.encode('iso8859-1') for t in expected_text]
         get_cursor = w.get_cursor_coords((4,))
         assert get_cursor == expected_cursor, "got: %r expected: %r" % (
             get_cursor, expected_cursor)
         r = w.render((4,), focus = 1)
         text = [t for a, cs, t in [ln[0] for ln in r.content()]]
-        assert text == expected_text, "got: %r expected: %r" % (text,
-                                                                expected_text)
+        assert text == expected_text, f"got: {text!r} expected: {expected_text!r}"
         assert r.cursor == expected_cursor, "got: %r expected: %r" % (
             r.cursor, expected_cursor)
 
diff --git a/urwid/tests/util.py b/urwid/tests/util.py
index 0c8c92828..4e593396f 100644
--- a/urwid/tests/util.py
+++ b/urwid/tests/util.py
@@ -1,5 +1,8 @@
+from __future__ import annotations
+
 import urwid
 
+
 class SelectableText(urwid.Text):
     def selectable(self):
         return True