Repository: python-cmd2/cmd2
Pull request #1092 (2021-04-07 16:04:52)

Updated main code to use f-strings

codecov[bot]: # [Codecov](https://codecov.io/gh/python-cmd2/cmd2/pull/1092?src=pr&el=h1) Report
> Merging [#1092](https://codecov.io/gh/python-cmd2/cmd2/pull/1092?src=pr&el=desc) (581e5dd) into [master](https://codecov.io/gh/python-cmd2/cmd2/commit/e8b6456fc73a6f7bdb176251a1539a1886659f32?el=desc) (e8b6456) will **decrease** coverage by `1.27%`.
> The diff coverage is `97.91%`.

[![Impacted file tree graph](https://codecov.io/gh/python-cmd2/cmd2/pull/1092/graphs/tree.svg?width=650&height=150&src=pr&token=eks2PUwa6A)](https://codecov.io/gh/python-cmd2/cmd2/pull/1092?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1092      +/-   ##
==========================================
- Coverage   98.35%   97.07%   -1.28%     
==========================================
  Files          22       22              
  Lines        4733     4724       -9     
==========================================
- Hits         4655     4586      -69     
- Misses         78      138      +60     
```


| [Impacted Files](https://codecov.io/gh/python-cmd2/cmd2/pull/1092?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [cmd2/decorators.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1092/diff?src=pr&el=tree#diff-Y21kMi9kZWNvcmF0b3JzLnB5) | `100.00% <ø> (ø)` | |
| [cmd2/transcript.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1092/diff?src=pr&el=tree#diff-Y21kMi90cmFuc2NyaXB0LnB5) | `91.96% <66.66%> (-1.02%)` | :arrow_down: |
| [cmd2/cmd2.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1092/diff?src=pr&el=tree#diff-Y21kMi9jbWQyLnB5) | `96.76% <98.63%> (-1.35%)` | :arrow_down: |
| [cmd2/ansi.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1092/diff?src=pr&el=tree#diff-Y21kMi9hbnNpLnB5) | `100.00% <100.00%> (ø)` | |
| [cmd2/argparse\_custom.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1092/diff?src=pr&el=tree#diff-Y21kMi9hcmdwYXJzZV9jdXN0b20ucHk=) | `94.54% <100.00%> (-0.63%)` | :arrow_down: |
| [cmd2/parsing.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1092/diff?src=pr&el=tree#diff-Y21kMi9wYXJzaW5nLnB5) | `99.28% <100.00%> (-0.72%)` | :arrow_down: |
| [cmd2/table\_creator.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1092/diff?src=pr&el=tree#diff-Y21kMi90YWJsZV9jcmVhdG9yLnB5) | `100.00% <100.00%> (ø)` | |
| [cmd2/utils.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1092/diff?src=pr&el=tree#diff-Y21kMi91dGlscy5weQ==) | `96.99% <100.00%> (-0.94%)` | :arrow_down: |
| [cmd2/rl\_utils.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1092/diff?src=pr&el=tree#diff-Y21kMi9ybF91dGlscy5weQ==) | `58.53% <0.00%> (-41.47%)` | :arrow_down: |
| [cmd2/clipboard.py](https://codecov.io/gh/python-cmd2/cmd2/pull/1092/diff?src=pr&el=tree#diff-Y21kMi9jbGlwYm9hcmQucHk=) | `71.42% <0.00%> (-28.58%)` | :arrow_down: |
| ... and [6 more](https://codecov.io/gh/python-cmd2/cmd2/pull/1092/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/python-cmd2/cmd2/pull/1092?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/python-cmd2/cmd2/pull/1092?src=pr&el=footer). Last update [e8b6456...581e5dd](https://codecov.io/gh/python-cmd2/cmd2/pull/1092?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/cmd2/ansi.py b/cmd2/ansi.py
index 5b086c45..32b50697 100644
--- a/cmd2/ansi.py
+++ b/cmd2/ansi.py
@@ -78,7 +78,7 @@ class ColorBase(Enum):
         """
         Return ANSI color sequence instead of enum name
         This is helpful when using a ColorBase in an f-string or format() call
-        e.g. my_str = "{}hello{}".format(fg.blue, fg.reset)
+        e.g. my_str = f"{fg.blue}hello{fg.reset}"
         """
         return str(self.value)
 
@@ -244,7 +244,7 @@ def fg_lookup(fg_name: Union[str, fg]) -> str:
     try:
         ansi_escape = fg[fg_name.lower()].value
     except KeyError:
-        raise ValueError('Foreground color {!r} does not exist; must be one of: {}'.format(fg_name, fg.colors()))
+        raise ValueError(f"Foreground color '{fg_name}' does not exist; must be one of: {fg.colors()}")
     return str(ansi_escape)
 
 
@@ -262,7 +262,7 @@ def bg_lookup(bg_name: Union[str, bg]) -> str:
     try:
         ansi_escape = bg[bg_name.lower()].value
     except KeyError:
-        raise ValueError('Background color {!r} does not exist; must be one of: {}'.format(bg_name, bg.colors()))
+        raise ValueError(f"Background color '{bg_name}' does not exist; must be one of: {bg.colors()}")
     return str(ansi_escape)
 
 
@@ -274,14 +274,14 @@ def style(
     bg: Union[str, bg] = '',
     bold: bool = False,
     dim: bool = False,
-    underline: bool = False
+    underline: bool = False,
 ) -> str:
     """
     Apply ANSI colors and/or styles to a string and return it.
     The styling is self contained which means that at the end of the string reset code(s) are issued
     to undo whatever styling was done at the beginning.
 
-    :param text: Any object compatible with str.format()
+    :param text: text to format (anything convertible to a str)
     :param fg: foreground color. Relies on `fg_lookup()` to retrieve ANSI escape based on name or enum.
                Defaults to no color.
     :param bg: background color. Relies on `bg_lookup()` to retrieve ANSI escape based on name or enum.
@@ -298,7 +298,7 @@ def style(
     removals: List[str] = []
 
     # Convert the text object into a string if it isn't already one
-    text_formatted = "{}".format(text)
+    text_formatted = str(text)
 
     # Process the style settings
     if fg:
diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py
index 6b1c799d..4ba05985 100644
--- a/cmd2/argparse_custom.py
+++ b/cmd2/argparse_custom.py
@@ -256,18 +256,16 @@ def generate_range_error(range_min: int, range_max: Union[int, float]) -> str:
     err_str = "expected "
 
     if range_max == constants.INFINITY:
-        err_str += "at least {} argument".format(range_min)
-
-        if range_min != 1:
-            err_str += "s"
+        plural = '' if range_min == 1 else 's'
+        err_str += f"at least {range_min}"
     else:
+        plural = '' if range_max == 1 else 's'
         if range_min == range_max:
-            err_str += "{} argument".format(range_min)
+            err_str += f"{range_min}"
         else:
-            err_str += "{} to {} argument".format(range_min, range_max)
+            err_str += f"{range_min} to {range_max}"
 
-        if range_max != 1:
-            err_str += "s"
+    err_str += f" argument{plural}"
 
     return err_str
 
@@ -600,7 +598,7 @@ def _get_nargs_pattern_wrapper(self: argparse.ArgumentParser, action: argparse.A
         else:
             range_max = nargs_range[1]
 
-        nargs_pattern = '(-*A{{{},{}}}-*)'.format(nargs_range[0], range_max)
+        nargs_pattern = f'(-*A{{{nargs_range[0]},{range_max}}}-*)'
 
         # if this is an optional action, -- is not allowed
         if action.option_strings:
@@ -881,9 +879,9 @@ class Cmd2HelpFormatter(argparse.RawTextHelpFormatter):
         nargs_range = getattr(action, ATTR_NARGS_RANGE, None)
         if nargs_range is not None:
             if nargs_range[1] == constants.INFINITY:
-                range_str = '{}+'.format(nargs_range[0])
+                range_str = f'{nargs_range[0]}+'
             else:
-                range_str = '{}..{}'.format(nargs_range[0], nargs_range[1])
+                range_str = f'{nargs_range[0]}..{nargs_range[1]}'
 
             return '{}{{{}}}'.format('%s' % metavar_formatter(1), range_str)
 
@@ -960,7 +958,7 @@ class Cmd2ArgumentParser(argparse.ArgumentParser):
 
         self.print_usage(sys.stderr)
         formatted_message = ansi.style_error(formatted_message)
-        self.exit(2, '{}\n\n'.format(formatted_message))
+        self.exit(2, f'{formatted_message}\n\n')
 
     # noinspection PyProtectedMember
     def format_help(self) -> str:
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index ef288120..6702d1a7 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -397,9 +397,9 @@ class Cmd(cmd.Cmd):
         if startup_script:
             startup_script = os.path.abspath(os.path.expanduser(startup_script))
             if os.path.exists(startup_script):
-                script_cmd = "run_script {}".format(utils.quote_string(startup_script))
+                script_cmd = f"run_script {utils.quote_string(startup_script)}"
                 if silent_startup_script:
-                    script_cmd += " > {}".format(os.devnull)
+                    script_cmd += f" {constants.REDIRECTION_OUTPUT} {os.devnull}"
                 self._startup_commands.append(script_cmd)
 
         # Transcript files to run instead of interactive command loop
@@ -520,7 +520,7 @@ class Cmd(cmd.Cmd):
         for cur_cmd in self.get_all_commands():
             valid, errmsg = self.statement_parser.is_valid_command(cur_cmd)
             if not valid:
-                raise ValueError("Invalid command name {!r}: {}".format(cur_cmd, errmsg))
+                raise ValueError(f"Invalid command name '{cur_cmd}': {errmsg}")
 
         # Add functions decorated to be subcommands
         self._register_subcommands(self)
@@ -651,21 +651,21 @@ class Cmd(cmd.Cmd):
 
         # Make sure command function doesn't share name with existing attribute
         if hasattr(self, cmd_func_name):
-            raise CommandSetRegistrationError('Attribute already exists: {} ({})'.format(cmd_func_name, context))
+            raise CommandSetRegistrationError(f'Attribute already exists: {cmd_func_name} ({context})')
 
         # Check if command has an invalid name
         valid, errmsg = self.statement_parser.is_valid_command(command)
         if not valid:
-            raise CommandSetRegistrationError("Invalid command name {!r}: {}".format(command, errmsg))
+            raise CommandSetRegistrationError(f"Invalid command name '{command}': {errmsg}")
 
         # Check if command shares a name with an alias
         if command in self.aliases:
-            self.pwarning("Deleting alias '{}' because it shares its name with a new command".format(command))
+            self.pwarning(f"Deleting alias '{command}' because it shares its name with a new command")
             del self.aliases[command]
 
         # Check if command shares a name with a macro
         if command in self.macros:
-            self.pwarning("Deleting macro '{}' because it shares its name with a new command".format(command))
+            self.pwarning(f"Deleting macro '{command}' because it shares its name with a new command")
             del self.macros[command]
 
         setattr(self, cmd_func_name, command_wrapper)
@@ -674,14 +674,14 @@ class Cmd(cmd.Cmd):
         completer_func_name = COMPLETER_FUNC_PREFIX + cmd_name
 
         if hasattr(self, completer_func_name):
-            raise CommandSetRegistrationError('Attribute already exists: {}'.format(completer_func_name))
+            raise CommandSetRegistrationError(f'Attribute already exists: {completer_func_name}')
         setattr(self, completer_func_name, cmd_completer)
 
     def _install_help_function(self, cmd_name: str, cmd_help: Callable[..., None]) -> None:
         help_func_name = HELP_FUNC_PREFIX + cmd_name
 
         if hasattr(self, help_func_name):
-            raise CommandSetRegistrationError('Attribute already exists: {}'.format(help_func_name))
+            raise CommandSetRegistrationError(f'Attribute already exists: {help_func_name}')
         setattr(self, help_func_name, cmd_help)
 
     def unregister_command_set(self, cmdset: CommandSet) -> None:
@@ -783,7 +783,7 @@ class Cmd(cmd.Cmd):
 
             subcommand_valid, errmsg = self.statement_parser.is_valid_command(subcommand_name, is_subcommand=True)
             if not subcommand_valid:
-                raise CommandSetRegistrationError('Subcommand {} is not valid: {}'.format(str(subcommand_name), errmsg))
+                raise CommandSetRegistrationError(f'Subcommand {str(subcommand_name)} is not valid: {errmsg}')
 
             command_tokens = full_command_name.split()
             command_name = command_tokens[0]
@@ -797,12 +797,12 @@ class Cmd(cmd.Cmd):
 
             if command_func is None:
                 raise CommandSetRegistrationError(
-                    'Could not find command "{}" needed by subcommand: {}'.format(command_name, str(method))
+                    f"Could not find command '{command_name}' needed by subcommand: {str(method)}"
                 )
             command_parser = getattr(command_func, constants.CMD_ATTR_ARGPARSER, None)
             if command_parser is None:
                 raise CommandSetRegistrationError(
-                    'Could not find argparser for command "{}" needed by subcommand: {}'.format(command_name, str(method))
+                    f"Could not find argparser for command '{command_name}' needed by subcommand: {str(method)}"
                 )
 
             def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) -> argparse.ArgumentParser:
@@ -815,7 +815,7 @@ class Cmd(cmd.Cmd):
                             if choice_name == cur_subcmd:
                                 return find_subcommand(choice, subcmd_names)
                         break
-                raise CommandSetRegistrationError('Could not find subcommand "{}"'.format(full_command_name))
+                raise CommandSetRegistrationError(f"Could not find subcommand '{full_command_name}'")
 
             target_parser = find_subcommand(command_parser, subcommand_names)
 
@@ -895,14 +895,14 @@ class Cmd(cmd.Cmd):
                 # This really shouldn't be possible since _register_subcommands would prevent this from happening
                 # but keeping in case it does for some strange reason
                 raise CommandSetRegistrationError(
-                    'Could not find command "{}" needed by subcommand: {}'.format(command_name, str(method))
+                    f"Could not find command '{command_name}' needed by subcommand: {str(method)}"
                 )
             command_parser = getattr(command_func, constants.CMD_ATTR_ARGPARSER, None)
             if command_parser is None:  # pragma: no cover
                 # This really shouldn't be possible since _register_subcommands would prevent this from happening
                 # but keeping in case it does for some strange reason
                 raise CommandSetRegistrationError(
-                    'Could not find argparser for command "{}" needed by subcommand: {}'.format(command_name, str(method))
+                    f"Could not find argparser for command '{command_name}' needed by subcommand: {str(method)}"
                 )
 
             for action in command_parser._actions:
@@ -983,7 +983,7 @@ class Cmd(cmd.Cmd):
                 'allow_style',
                 str,
                 'Allow ANSI text style sequences in output (valid values: '
-                '{}, {}, {})'.format(ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, ansi.STYLE_NEVER),
+                f'{ansi.STYLE_TERMINAL}, {ansi.STYLE_ALWAYS}, {ansi.STYLE_NEVER})',
                 self,
                 choices=[ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, ansi.STYLE_NEVER],
             )
@@ -1021,9 +1021,7 @@ class Cmd(cmd.Cmd):
         if new_val in [ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, ansi.STYLE_NEVER]:
             ansi.allow_style = new_val
         else:
-            raise ValueError(
-                "must be {}, {}, or {} (case-insensitive)".format(ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, ansi.STYLE_NEVER)
-            )
+            raise ValueError(f"must be {ansi.STYLE_TERMINAL}, {ansi.STYLE_ALWAYS}, or {ansi.STYLE_NEVER} (case-insensitive)")
 
     def _completion_supported(self) -> bool:
         """Return whether tab completion is supported"""
@@ -1047,11 +1045,11 @@ class Cmd(cmd.Cmd):
         been piped to another process and that process terminates before the
         cmd2 command is finished executing.
 
-        :param msg: message to print (anything convertible to a str with '{}'.format() is OK)
+        :param msg: message to print (anything convertible to a str)
         :param end: string appended after the end of the message, default a newline
         """
         try:
-            ansi.style_aware_write(self.stdout, "{}{}".format(msg, end))
+            ansi.style_aware_write(self.stdout, f"{msg}{end}")
         except BrokenPipeError:
             # This occurs if a command's output is being piped to another
             # process and that process closes before the command is
@@ -1065,7 +1063,7 @@ class Cmd(cmd.Cmd):
     def perror(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) -> None:
         """Print message to sys.stderr
 
-        :param msg: message to print (anything convertible to a str with '{}'.format() is OK)
+        :param msg: message to print (anything convertible to a str)
         :param end: string appended after the end of the message, default a newline
         :param apply_style: If True, then ansi.style_error will be applied to the message text. Set to False in cases
                             where the message text already has the desired style. Defaults to True.
@@ -1073,13 +1071,13 @@ class Cmd(cmd.Cmd):
         if apply_style:
             final_msg = ansi.style_error(msg)
         else:
-            final_msg = "{}".format(msg)
+            final_msg = str(msg)
         ansi.style_aware_write(sys.stderr, final_msg + end)
 
     def pwarning(self, msg: Any = '', *, end: str = '\n', apply_style: bool = True) -> None:
         """Wraps perror, but applies ansi.style_warning by default
 
-        :param msg: message to print (anything convertible to a str with '{}'.format() is OK)
+        :param msg: message to print (anything convertible to a str)
         :param end: string appended after the end of the message, default a newline
         :param apply_style: If True, then ansi.style_warning will be applied to the message text. Set to False in cases
                             where the message text already has the desired style. Defaults to True.
@@ -1102,9 +1100,9 @@ class Cmd(cmd.Cmd):
             traceback.print_exc()
 
         if isinstance(msg, Exception):
-            final_msg = "EXCEPTION of type '{}' occurred with message: '{}'".format(type(msg).__name__, msg)
+            final_msg = f"EXCEPTION of type '{type(msg).__name__}' occurred with message: {msg}"
         else:
-            final_msg = "{}".format(msg)
+            final_msg = str(msg)
 
         if apply_style:
             final_msg = ansi.style_error(final_msg)
@@ -1119,7 +1117,7 @@ class Cmd(cmd.Cmd):
         """For printing nonessential feedback.  Can be silenced with `quiet`.
         Inclusion in redirected output is controlled by `feedback_to_output`.
 
-        :param msg: message to print (anything convertible to a str with '{}'.format() is OK)
+        :param msg: message to print (anything convertible to a str)
         :param end: string appended after the end of the message, default a newline
         """
         if not self.quiet:
@@ -1134,7 +1132,7 @@ class Cmd(cmd.Cmd):
         Never uses a pager inside of a script (Python or text) or when output is being redirected or piped or when
         stdout or stdin are not a fully functional terminal.
 
-        :param msg: message to print to current stdout (anything convertible to a str with '{}'.format() is OK)
+        :param msg: message to print to current stdout (anything convertible to a str)
         :param end: string appended after the end of the message, default a newline
         :param chop: True -> causes lines longer than the screen width to be chopped (truncated) rather than wrapped
                               - truncated text is still accessible by scrolling with the right & left arrow keys
@@ -2341,7 +2339,7 @@ class Cmd(cmd.Cmd):
                 stop = self.postcmd(stop, statement)
 
                 if self.timing:
-                    self.pfeedback('Elapsed: {}'.format(datetime.datetime.now() - timestart))
+                    self.pfeedback(f'Elapsed: {datetime.datetime.now() - timestart}')
             finally:
                 # Get sigint protection while we restore stuff
                 with self.sigint_protection:
@@ -2355,7 +2353,7 @@ class Cmd(cmd.Cmd):
             # Don't do anything, but do allow command finalization hooks to run
             pass
         except Cmd2ShlexError as ex:
-            self.perror("Invalid syntax: {}".format(ex))
+            self.perror(f"Invalid syntax: {ex}")
         except RedirectionError as ex:
             self.perror(ex)
         except KeyboardInterrupt as ex:
@@ -2424,7 +2422,7 @@ class Cmd(cmd.Cmd):
                 line = line.raw
 
             if self.echo:
-                self.poutput('{}{}'.format(self.prompt, line))
+                self.poutput(f'{self.prompt}{line}')
 
             try:
                 if self.onecmd_plus_hooks(
@@ -2486,7 +2484,7 @@ class Cmd(cmd.Cmd):
                     # terminator
                     nextline = '\n'
                     self.poutput(nextline)
-                line = '{}{}'.format(self._multiline_in_progress, nextline)
+                line = f'{self._multiline_in_progress}{nextline}'
             except KeyboardInterrupt as ex:
                 if self.quit_on_sigint:
                     raise ex
@@ -2558,13 +2556,14 @@ class Cmd(cmd.Cmd):
         :return: the resolved macro or None on error
         """
         if statement.command not in self.macros.keys():
-            raise KeyError('{} is not a macro'.format(statement.command))
+            raise KeyError(f"{statement.command} is not a macro")
 
         macro = self.macros[statement.command]
 
         # Make sure enough arguments were passed in
         if len(statement.arg_list) < macro.minimum_arg_count:
-            self.perror("The macro '{}' expects at least {} argument(s)".format(statement.command, macro.minimum_arg_count))
+            plural = '' if macro.minimum_arg_count == 1 else 's'
+            self.perror(f"The macro '{statement.command}' expects at least {macro.minimum_arg_count} argument{plural}")
             return None
 
         # Resolve the arguments in reverse and read their values from statement.argv since those
@@ -2652,7 +2651,7 @@ class Cmd(cmd.Cmd):
             if proc.returncode is not None:
                 subproc_stdin.close()
                 new_stdout.close()
-                raise RedirectionError('Pipe process exited with code {} before command could run'.format(proc.returncode))
+                raise RedirectionError(f'Pipe process exited with code {proc.returncode} before command could run')
             else:
                 redir_saved_state.redirecting = True
                 cmd_pipe_proc_reader = utils.ProcReader(proc, cast(TextIO, self.stdout), sys.stderr)
@@ -2935,7 +2934,7 @@ class Cmd(cmd.Cmd):
             else:
                 line = input()
                 if self.echo:
-                    sys.stdout.write('{}{}\n'.format(prompt, line))
+                    sys.stdout.write(f'{prompt}{line}\n')
 
         # Otherwise read from self.stdin
         else:
@@ -2954,7 +2953,7 @@ class Cmd(cmd.Cmd):
                 if len(line):
                     # we read something, output the prompt and the something
                     if self.echo:
-                        self.poutput('{}{}'.format(prompt, line))
+                        self.poutput(f'{prompt}{line}')
                 else:
                     line = 'eof'
 
@@ -3124,7 +3123,7 @@ class Cmd(cmd.Cmd):
         # Validate the alias name
         valid, errmsg = self.statement_parser.is_valid_command(args.name)
         if not valid:
-            self.perror("Invalid alias name: {}".format(errmsg))
+            self.perror(f"Invalid alias name: {errmsg}")
             return
 
         if args.name in self.get_all_commands():
@@ -3147,7 +3146,7 @@ class Cmd(cmd.Cmd):
 
         # Set the alias
         result = "overwritten" if args.name in self.aliases else "created"
-        self.poutput("Alias '{}' {}".format(args.name, result))
+        self.poutput(f"Alias '{args.name}' {result}")
 
         self.aliases[args.name] = value
 
@@ -3177,9 +3176,9 @@ class Cmd(cmd.Cmd):
             for cur_name in utils.remove_duplicates(args.names):
                 if cur_name in self.aliases:
                     del self.aliases[cur_name]
-                    self.poutput("Alias '{}' deleted".format(cur_name))
+                    self.poutput(f"Alias '{cur_name}' deleted")
                 else:
-                    self.perror("Alias '{}' does not exist".format(cur_name))
+                    self.perror(f"Alias '{cur_name}' does not exist")
 
     # alias -> list
     alias_list_help = "list aliases"
@@ -3226,10 +3225,10 @@ class Cmd(cmd.Cmd):
             if args:
                 val += ' ' + ' '.join(command_args)
 
-            self.poutput("alias create {} {}".format(name, val))
+            self.poutput(f"alias create {name} {val}")
 
         for name in not_found:
-            self.perror("Alias '{}' not found".format(name))
+            self.perror(f"Alias '{name}' not found")
 
     #############################################################
     # Parsers and functions for macro command and subcommands
@@ -3306,7 +3305,7 @@ class Cmd(cmd.Cmd):
         # Validate the macro name
         valid, errmsg = self.statement_parser.is_valid_command(args.name)
         if not valid:
-            self.perror("Invalid macro name: {}".format(errmsg))
+            self.perror(f"Invalid macro name: {errmsg}")
             return
 
         if args.name in self.get_all_commands():
@@ -3355,7 +3354,7 @@ class Cmd(cmd.Cmd):
 
         # Make sure the argument numbers are continuous
         if len(arg_nums) != max_arg_num:
-            self.perror("Not all numbers between 1 and {} are present " "in the argument placeholders".format(max_arg_num))
+            self.perror(f"Not all numbers between 1 and {max_arg_num} are present in the argument placeholders")
             return
 
         # Find all escaped arguments
@@ -3374,7 +3373,7 @@ class Cmd(cmd.Cmd):
 
         # Set the macro
         result = "overwritten" if args.name in self.macros else "created"
-        self.poutput("Macro '{}' {}".format(args.name, result))
+        self.poutput(f"Macro '{args.name}' {result}")
 
         self.macros[args.name] = Macro(name=args.name, value=value, minimum_arg_count=max_arg_num, arg_list=arg_list)
 
@@ -3403,9 +3402,9 @@ class Cmd(cmd.Cmd):
             for cur_name in utils.remove_duplicates(args.names):
                 if cur_name in self.macros:
                     del self.macros[cur_name]
-                    self.poutput("Macro '{}' deleted".format(cur_name))
+                    self.poutput(f"Macro '{cur_name}' deleted")
                 else:
-                    self.perror("Macro '{}' does not exist".format(cur_name))
+                    self.perror(f"Macro '{cur_name}' does not exist")
 
     # macro -> list
     macro_list_help = "list macros"
@@ -3452,10 +3451,10 @@ class Cmd(cmd.Cmd):
             if command_args:
                 val += ' ' + ' '.join(command_args)
 
-            self.poutput("macro create {} {}".format(name, val))
+            self.poutput(f"macro create {name} {val}")
 
         for name in not_found:
-            self.perror("Macro '{}' not found".format(name))
+            self.perror(f"Macro '{name}' not found")
 
     def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
         """Completes the command argument of help"""
@@ -3548,14 +3547,14 @@ class Cmd(cmd.Cmd):
         """Show a list of commands which help can be displayed for"""
         cmds_cats, cmds_doc, cmds_undoc, help_topics = self._build_command_info()
 
-        if len(cmds_cats) == 0:
+        if not cmds_cats:
             # No categories found, fall back to standard behavior
-            self.poutput("{}".format(str(self.doc_leader)))
+            self.poutput(self.doc_leader)
             self._print_topics(self.doc_header, cmds_doc, verbose)
         else:
             # Categories found, Organize all commands by category
-            self.poutput('{}'.format(str(self.doc_leader)))
-            self.poutput('{}'.format(str(self.doc_header)), end="\n\n")
+            self.poutput(self.doc_leader)
+            self.poutput(self.doc_header, end="\n\n")
             for category in sorted(cmds_cats.keys(), key=self.default_sort_key):
                 self._print_topics(category, cmds_cats[category], verbose)
             self._print_topics(self.default_category, cmds_doc, verbose)
@@ -3601,7 +3600,7 @@ class Cmd(cmd.Cmd):
             if not verbose:
                 self.print_topics(header, cmds, 15, 80)
             else:
-                self.stdout.write('{}\n'.format(str(header)))
+                self.poutput(f'{header}')
                 widest = 0
                 # measure the commands
                 for command in cmds:
@@ -3614,7 +3613,8 @@ class Cmd(cmd.Cmd):
                     widest = 20
 
                 if self.ruler:
-                    self.stdout.write('{:{ruler}<{width}}\n'.format('', ruler=self.ruler, width=80))
+                    ruler_line = utils.align_left('', width=80, fill_char=self.ruler)
+                    self.poutput(f'{ruler_line}')
 
                 # Try to get the documentation string for each command
                 topics = self.get_help_topics()
@@ -3664,9 +3664,9 @@ class Cmd(cmd.Cmd):
                                 break
 
                     for doc_line in doc_block:
-                        self.stdout.write('{: <{col_width}}{doc}\n'.format(command, col_width=widest, doc=doc_line))
+                        self.poutput(f'{command: <{widest}}{doc_line}')
                         command = ''
-                self.stdout.write("\n")
+                self.poutput()
 
     shortcuts_parser = DEFAULT_ARGUMENT_PARSER(description="List available shortcuts")
 
@@ -3676,7 +3676,7 @@ class Cmd(cmd.Cmd):
         # Sort the shortcut tuples by name
         sorted_shortcuts = sorted(self.statement_parser.shortcuts, key=lambda x: self.default_sort_key(x[0]))
         result = "\n".join('{}: {}'.format(sc[0], sc[1]) for sc in sorted_shortcuts)
-        self.poutput("Shortcuts for other commands:\n{}".format(result))
+        self.poutput(f"Shortcuts for other commands:\n{result}")
 
     eof_parser = DEFAULT_ARGUMENT_PARSER(description="Called when Ctrl-D is pressed", epilog=INTERNAL_COMMAND_EPILOG)
 
@@ -3741,7 +3741,7 @@ class Cmd(cmd.Cmd):
                     raise IndexError
                 return str(fulloptions[choice - 1][0])
             except (ValueError, IndexError):
-                self.poutput("{!r} isn't a valid choice. Pick a number between 1 and {}:".format(response, len(fulloptions)))
+                self.poutput(f"'{response}' isn't a valid choice. Pick a number between 1 and {len(fulloptions)}:")
 
     def complete_set_value(
         self, text: str, line: str, begidx: int, endidx: int, arg_tokens: Dict[str, List[str]]
@@ -3815,7 +3815,7 @@ class Cmd(cmd.Cmd):
             try:
                 settable = self.settables[args.param]
             except KeyError:
-                self.perror("Parameter '{}' not supported (type 'set' for list of parameters).".format(args.param))
+                self.perror(f"Parameter '{args.param}' not supported (type 'set' for list of parameters).")
                 return
 
             if args.value:
@@ -3825,10 +3825,9 @@ class Cmd(cmd.Cmd):
                     new_value = settable.set_value(utils.strip_quotes(args.value))
                 # noinspection PyBroadException
                 except Exception as e:
-                    err_msg = "Error setting {}: {}".format(args.param, e)
-                    self.perror(err_msg)
+                    self.perror(f"Error setting {args.param}: {e}")
                 else:
-                    self.poutput('{} - was: {!r}\nnow: {!r}'.format(args.param, orig_value, new_value))
+                    self.poutput(f"{args.param} - was: {orig_value!r}\nnow: {new_value!r}")
                 return
 
             # Show one settable
@@ -3842,14 +3841,14 @@ class Cmd(cmd.Cmd):
         results = dict()
         for param in to_show:
             settable = self.settables[param]
-            results[param] = '{}: {!r}'.format(param, settable.get_value())
+            results[param] = f"{param}: {settable.get_value()!r}"
             max_len = max(max_len, ansi.style_aware_wcswidth(results[param]))
 
         # Display the results
         for param in sorted(results, key=self.default_sort_key):
             result_str = results[param]
             if args.verbose:
-                self.poutput('{} # {}'.format(utils.align_left(result_str, width=max_len), self.settables[param].description))
+                self.poutput(f'{utils.align_left(result_str, width=max_len)} # {self.settables[param].description}')
             else:
                 self.poutput(result_str)
 
@@ -4368,7 +4367,7 @@ class Cmd(cmd.Cmd):
                             fobj.write(f"{item.expanded}\n")
                         else:
                             fobj.write(f"{item.raw}\n")
-                plural = 's' if len(history) > 1 else ''
+                plural = '' if len(history) == 1 else 's'
             except OSError as ex:
                 self.perror(f"Error saving history file '{full_path}': {ex}")
             else:
@@ -4575,10 +4574,10 @@ class Cmd(cmd.Cmd):
             self.perror(f"Error saving transcript file '{transcript_path}': {ex}")
         else:
             # and let the user know what we did
-            if commands_run > 1:
-                plural = 'commands and their outputs'
-            else:
+            if commands_run == 1:
                 plural = 'command and its output'
+            else:
+                plural = 'commands and their outputs'
             self.pfeedback(f"{commands_run} {plural} saved to transcript file '{transcript_path}'")
 
     edit_description = (
@@ -4753,10 +4752,10 @@ class Cmd(cmd.Cmd):
         num_transcripts = len(transcripts_expanded)
         plural = '' if len(transcripts_expanded) == 1 else 's'
         self.poutput(ansi.style(utils.align_center(' cmd2 transcript test ', fill_char='='), bold=True))
-        self.poutput('platform {} -- Python {}, cmd2-{}, readline-{}'.format(sys.platform, verinfo, cmd2.__version__, rl_type))
-        self.poutput('cwd: {}'.format(os.getcwd()))
-        self.poutput('cmd2 app: {}'.format(sys.argv[0]))
-        self.poutput(ansi.style('collected {} transcript{}'.format(num_transcripts, plural), bold=True))
+        self.poutput(f'platform {sys.platform} -- Python {verinfo}, cmd2-{cmd2.__version__}, readline-{rl_type}')
+        self.poutput(f'cwd: {os.getcwd()}')
+        self.poutput(f'cmd2 app: {sys.argv[0]}')
+        self.poutput(ansi.style(f'collected {num_transcripts} transcript{plural}', bold=True))
 
         setattr(self.__class__, 'testfiles', transcripts_expanded)
         sys.argv = [sys.argv[0]]  # the --test argument upsets unittest.main()
@@ -4769,7 +4768,7 @@ class Cmd(cmd.Cmd):
         execution_time = time.time() - start_time
         if test_results.wasSuccessful():
             ansi.style_aware_write(sys.stderr, stream.read())
-            finish_msg = ' {0} transcript{1} passed in {2:.3f} seconds '.format(num_transcripts, plural, execution_time)
+            finish_msg = f' {num_transcripts} transcript{plural} passed in {execution_time:.3f} seconds '
             finish_msg = ansi.style_success(utils.align_center(finish_msg, fill_char='='))
             self.poutput(finish_msg)
         else:
@@ -4949,9 +4948,9 @@ class Cmd(cmd.Cmd):
         :param command: the command being disabled
         :param message_to_print: what to print when this command is run or help is called on it while disabled
 
-                                 The variable COMMAND_NAME can be used as a placeholder for the name of the
+                                 The variable cmd2.COMMAND_NAME can be used as a placeholder for the name of the
                                  command being disabled.
-                                 ex: message_to_print = "{} is currently disabled".format(COMMAND_NAME)
+                                 ex: message_to_print = f"{cmd2.COMMAND_NAME} is currently disabled"
         """
         # If the commands is already disabled, then return
         if command in self.disabled_commands:
@@ -4960,7 +4959,7 @@ class Cmd(cmd.Cmd):
         # Make sure this is an actual command
         command_function = self.cmd_func(command)
         if command_function is None:
-            raise AttributeError("{} does not refer to a command".format(command))
+            raise AttributeError(f"'{command}' does not refer to a command")
 
         help_func_name = constants.HELP_FUNC_PREFIX + command
         completer_func_name = constants.COMPLETER_FUNC_PREFIX + command
@@ -4987,9 +4986,9 @@ class Cmd(cmd.Cmd):
 
         :param category: the category to disable
         :param message_to_print: what to print when anything in this category is run or help is called on it
-                                 while disabled. The variable COMMAND_NAME can be used as a placeholder for the name
+                                 while disabled. The variable cmd2.COMMAND_NAME can be used as a placeholder for the name
                                  of the command being disabled.
-                                 ex: message_to_print = "{} is currently disabled".format(COMMAND_NAME)
+                                 ex: message_to_print = f"{cmd2.COMMAND_NAME} is currently disabled"
         """
         all_commands = self.get_all_commands()
 
@@ -5089,13 +5088,8 @@ class Cmd(cmd.Cmd):
         # validate that the callable has the right number of parameters
         nparam = len(signature.parameters)
         if nparam != count:
-            raise TypeError(
-                '{} has {} positional arguments, expected {}'.format(
-                    func.__name__,
-                    nparam,
-                    count,
-                )
-            )
+            plural = '' if nparam == 1 else 's'
+            raise TypeError(f'{func.__name__} has {nparam} positional argument{plural}, expected {count}')
 
     @classmethod
     def _validate_prepostloop_callable(cls, func: Callable[[], None]) -> None:
@@ -5104,11 +5098,7 @@ class Cmd(cmd.Cmd):
         # make sure there is no return notation
         signature = inspect.signature(func)
         if signature.return_annotation is not None:
-            raise TypeError(
-                "{} must declare return a return type of 'None'".format(
-                    func.__name__,
-                )
-            )
+            raise TypeError(f"{func.__name__} must declare return a return type of 'None'")
 
     def register_preloop_hook(self, func: Callable[[], None]) -> None:
         """Register a function to be called at the beginning of the command loop."""
@@ -5127,11 +5117,9 @@ class Cmd(cmd.Cmd):
         signature = inspect.signature(func)
         _, param = list(signature.parameters.items())[0]
         if param.annotation != plugin.PostparsingData:
-            raise TypeError(
-                "{} must have one parameter declared with type 'cmd2.plugin.PostparsingData'".format(func.__name__)
-            )
+            raise TypeError(f"{func.__name__} must have one parameter declared with type 'cmd2.plugin.PostparsingData'")
         if signature.return_annotation != plugin.PostparsingData:
-            raise TypeError("{} must declare return a return type of 'cmd2.plugin.PostparsingData'".format(func.__name__))
+            raise TypeError(f"{func.__name__} must declare return a return type of 'cmd2.plugin.PostparsingData'")
 
     def register_postparsing_hook(self, func: Callable[[plugin.PostparsingData], plugin.PostparsingData]) -> None:
         """Register a function to be called after parsing user input but before running the command"""
@@ -5152,28 +5140,13 @@ class Cmd(cmd.Cmd):
         paramname = list(signature.parameters.keys())[0]
         param = signature.parameters[paramname]
         if param.annotation != data_type:
-            raise TypeError(
-                'argument 1 of {} has incompatible type {}, expected {}'.format(
-                    func.__name__,
-                    param.annotation,
-                    data_type,
-                )
-            )
+            raise TypeError(f'argument 1 of {func.__name__} has incompatible type {param.annotation}, expected {data_type}')
         # validate the return value has the right annotation
         if signature.return_annotation == signature.empty:
-            raise TypeError(
-                '{} does not have a declared return type, expected {}'.format(
-                    func.__name__,
-                    data_type,
-                )
-            )
+            raise TypeError(f'{func.__name__} does not have a declared return type, expected {data_type}')
         if signature.return_annotation != data_type:
             raise TypeError(
-                '{} has incompatible return type {}, expected {}'.format(
-                    func.__name__,
-                    signature.return_annotation,
-                    data_type,
-                )
+                f'{func.__name__} has incompatible return type {signature.return_annotation}, expected ' f'{data_type}'
             )
 
     def register_precmd_hook(self, func: Callable[[plugin.PrecommandData], plugin.PrecommandData]) -> None:
@@ -5195,11 +5168,9 @@ class Cmd(cmd.Cmd):
         signature = inspect.signature(func)
         _, param = list(signature.parameters.items())[0]
         if param.annotation != plugin.CommandFinalizationData:
-            raise TypeError(
-                "{} must have one parameter declared with type {}".format(func.__name__, plugin.CommandFinalizationData)
-            )
+            raise TypeError(f"{func.__name__} must have one parameter declared with type {plugin.CommandFinalizationData}")
         if signature.return_annotation != plugin.CommandFinalizationData:
-            raise TypeError("{} must declare return a return type of {}".format(func.__name__, plugin.CommandFinalizationData))
+            raise TypeError("{func.__name__} must declare return a return type of {plugin.CommandFinalizationData}")
 
     def register_cmdfinalization_hook(
         self, func: Callable[[plugin.CommandFinalizationData], plugin.CommandFinalizationData]
diff --git a/cmd2/decorators.py b/cmd2/decorators.py
index d5a7a190..2d57a5bf 100644
--- a/cmd2/decorators.py
+++ b/cmd2/decorators.py
@@ -298,7 +298,7 @@ def with_argparser(
     >>>     @cmd2.with_argparser(parser, preserve_quotes=True)
     >>>     def do_argprint(self, args):
     >>>         "Print the options and argument list this options command was called with."
-    >>>         self.poutput('args: {!r}'.format(args))
+    >>>         self.poutput(f'args: {args!r}')
 
     :Example with unknown args:
 
@@ -311,8 +311,8 @@ def with_argparser(
     >>>     @cmd2.with_argparser(parser, with_unknown_args=True)
     >>>     def do_argprint(self, args, unknown):
     >>>         "Print the options and argument list this options command was called with."
-    >>>         self.poutput('args: {!r}'.format(args))
-    >>>         self.poutput('unknowns: {}'.format(unknown))
+    >>>         self.poutput(f'args: {args!r}')
+    >>>         self.poutput(f'unknowns: {unknown}')
 
     """
     import functools
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index 46d32549..3893cb23 100755
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -167,7 +167,7 @@ class Statement(str):  # type: ignore[override]
         excluded, as are any command terminators.
         """
         if self.command and self.args:
-            rtn = '{} {}'.format(self.command, self.args)
+            rtn = f'{self.command} {self.args}'
         elif self.command:
             # there were no arguments to the command
             rtn = self.command
@@ -286,7 +286,7 @@ class StatementParser:
         # join them up with a pipe
         second_group = '|'.join(second_group_items)
         # build the regular expression
-        expr = r'\A\s*(\S*?)({})'.format(second_group)
+        expr = rf'\A\s*(\S*?)({second_group})'
         self._command_pattern = re.compile(expr)
 
     def is_valid_command(self, word: str, *, is_subcommand: bool = False) -> Tuple[bool, str]:
@@ -306,12 +306,12 @@ class StatementParser:
             checkit = '>'
             valid, errmsg = statement_parser.is_valid_command(checkit)
             if not valid:
-                errmsg = "alias: {}".format(errmsg)
+                errmsg = f"alias: {errmsg}"
         """
         valid = False
 
         if not isinstance(word, str):
-            return False, 'must be a string. Received {} instead'.format(str(type(word)))  # type: ignore[unreachable]
+            return False, f'must be a string. Received {str(type(word))} instead'  # type: ignore[unreachable]
 
         if not word:
             return False, 'cannot be an empty string'
diff --git a/cmd2/table_creator.py b/cmd2/table_creator.py
index ade40ae3..1a60e93b 100644
--- a/cmd2/table_creator.py
+++ b/cmd2/table_creator.py
@@ -66,7 +66,7 @@ class Column:
         header_vert_align: VerticalAlignment = VerticalAlignment.BOTTOM,
         data_horiz_align: HorizontalAlignment = HorizontalAlignment.LEFT,
         data_vert_align: VerticalAlignment = VerticalAlignment.TOP,
-        max_data_lines: Union[int, float] = constants.INFINITY
+        max_data_lines: Union[int, float] = constants.INFINITY,
     ) -> None:
         """
         Column initializer
@@ -408,7 +408,7 @@ class TableCreator:
         fill_char: str = SPACE,
         pre_line: str = EMPTY,
         inter_cell: str = (2 * SPACE),
-        post_line: str = EMPTY
+        post_line: str = EMPTY,
     ) -> str:
         """
         Generate a header or data table row
@@ -462,7 +462,7 @@ class TableCreator:
         validation_dict = {'fill_char': fill_char, 'pre_line': pre_line, 'inter_cell': inter_cell, 'post_line': post_line}
         for key, val in validation_dict.items():
             if ansi.style_aware_wcswidth(val) == -1:
-                raise (ValueError("{} contains an unprintable character".format(key)))
+                raise ValueError(f"{key} contains an unprintable character")
 
         # Number of lines this row uses
         total_lines = 0
@@ -556,7 +556,7 @@ class SimpleTable(TableCreator):
 
             divider_char_width = ansi.style_aware_wcswidth(divider_char)
             if divider_char_width == -1:
-                raise (ValueError("Divider character is an unprintable character"))
+                raise ValueError("Divider character is an unprintable character")
 
         super().__init__(cols, tab_width=tab_width)
         self.divider_char = divider_char
@@ -855,7 +855,7 @@ class AlternatingTable(BorderedTable):
         column_borders: bool = True,
         padding: int = 1,
         bg_odd: Optional[ansi.bg] = None,
-        bg_even: Optional[ansi.bg] = ansi.bg.bright_black
+        bg_even: Optional[ansi.bg] = ansi.bg.bright_black,
     ) -> None:
         """
         AlternatingTable initializer
diff --git a/cmd2/transcript.py b/cmd2/transcript.py
index 7ea5b8a9..83ede932 100644
--- a/cmd2/transcript.py
+++ b/cmd2/transcript.py
@@ -100,9 +100,7 @@ class Cmd2TestCase(unittest.TestCase):
                 try:
                     line = next(transcript)
                 except StopIteration as exc:
-                    msg = 'Transcript broke off while reading command beginning at line {} with\n{}'.format(
-                        line_num, command_parts[0]
-                    )
+                    msg = f'Transcript broke off while reading command beginning at line {line_num} with\n{command_parts[0]}'
                     raise StopIteration(msg) from exc
                 line_num += 1
             command = ''.join(command_parts)
@@ -112,9 +110,7 @@ class Cmd2TestCase(unittest.TestCase):
             stop_msg = 'Command indicated application should quit, but more commands in transcript'
             # Read the expected result from transcript
             if ansi.strip_style(line).startswith(self.cmdapp.visible_prompt):
-                message = '\nFile {}, line {}\nCommand was:\n{}\nExpected: (nothing)\nGot:\n{}\n'.format(
-                    fname, line_num, command, result
-                )
+                message = f'\nFile {fname}, line {line_num}\nCommand was:\n{command}\nExpected: (nothing)\nGot:\n{result}\n'
                 self.assertTrue(not (result.strip()), message)
                 # If the command signaled the application to quit there should be no more commands
                 self.assertFalse(stop, stop_msg)
@@ -136,9 +132,7 @@ class Cmd2TestCase(unittest.TestCase):
             # transform the expected text into a valid regular expression
             expected = ''.join(expected_parts)
             expected = self._transform_transcript_expected(expected)
-            message = '\nFile {}, line {}\nCommand was:\n{}\nExpected:\n{}\nGot:\n{}\n'.format(
-                fname, line_num, command, expected, result
-            )
+            message = f'\nFile {fname}, line {line_num}\nCommand was:\n{command}\nExpected:\n{expected}\nGot:\n{result}\n'
             self.assertTrue(re.match(expected, result, re.MULTILINE | re.DOTALL), message)
 
     def _transform_transcript_expected(self, s: str) -> str:
diff --git a/cmd2/utils.py b/cmd2/utils.py
index 4f3eae7b..80b0ee19 100644
--- a/cmd2/utils.py
+++ b/cmd2/utils.py
@@ -474,7 +474,7 @@ class StdSim:
         :param s: String to write to the stream
         """
         if not isinstance(s, str):
-            raise TypeError('write() argument must be str, not {}'.format(type(s)))
+            raise TypeError(f'write() argument must be str, not {type(s)}')
 
         if not self.pause_storage:
             self.buffer.byte_buf += s.encode(encoding=self.encoding, errors=self.errors)
@@ -554,7 +554,7 @@ class ByteBuf:
     def write(self, b: bytes) -> None:
         """Add bytes to internal bytes buffer and if echo is True, echo contents to inner stream."""
         if not isinstance(b, bytes):
-            raise TypeError('a bytes-like object is required, not {}'.format(type(b)))
+            raise TypeError(f'a bytes-like object is required, not {type(b)}')
         if not self.std_sim_instance.pause_storage:
             self.byte_buf += b
         if self.std_sim_instance.echo:


Test patch:
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 43c937ae..7595327f 100755
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -704,7 +704,7 @@ def _expected_no_editor_error():
 
     expected_text = normalize(
         """
-EXCEPTION of type '{}' occurred with message: 'Please use 'set editor' to specify your text editing program of choice.'
+EXCEPTION of type '{}' occurred with message: Please use 'set editor' to specify your text editing program of choice.
 To enable full traceback, run the following command: 'set debug true'
 """.format(
             expected_exception
@@ -1986,7 +1986,7 @@ def test_macro_usage_with_missing_args(base_app):
 
     # Run the macro
     out, err = run_cmd(base_app, 'fake arg1')
-    assert "expects at least 2 argument(s)" in err[0]
+    assert "expects at least 2 arguments" in err[0]
 
 
 def test_macro_usage_with_exta_args(base_app):
@@ -2018,7 +2018,7 @@ def test_macro_create_with_unicode_numbered_arg(base_app):
 
     # Run the macro
     out, err = run_cmd(base_app, 'fake')
-    assert "expects at least 1 argument(s)" in err[0]
+    assert "expects at least 1 argument" in err[0]
 
 
 def test_macro_create_with_missing_unicode_arg_nums(base_app):
@@ -2131,6 +2131,34 @@ def test_pwarning_no_style(base_app, capsys):
     assert err == msg + end
 
 
+def test_pexcept_style(base_app, capsys):
+    msg = Exception('testing...')
+    ansi.allow_style = ansi.STYLE_ALWAYS
+
+    base_app.pexcept(msg)
+    out, err = capsys.readouterr()
+    assert err.startswith(ansi.style_error("EXCEPTION of type 'Exception' occurred with message: testing..."))
+
+
+def test_pexcept_no_style(base_app, capsys):
+    msg = Exception('testing...')
+    ansi.allow_style = ansi.STYLE_ALWAYS
+
+    base_app.pexcept(msg, apply_style=False)
+    out, err = capsys.readouterr()
+    assert err.startswith("EXCEPTION of type 'Exception' occurred with message: testing...")
+
+
+def test_pexcept_not_exception(base_app, capsys):
+    # Pass in a msg that is not an Exception object
+    msg = False
+    ansi.allow_style = ansi.STYLE_ALWAYS
+
+    base_app.pexcept(msg)
+    out, err = capsys.readouterr()
+    assert err.startswith(ansi.style_error(msg))
+
+
 def test_ppaged(outsim_app):
     msg = 'testing...'
     end = '\n'