diff --git a/rbtools/commands/__init__.py b/rbtools/commands/__init__.py
index 3a1ce8f26ea1b9e6a9c2d725bd91d087c468374f..1325f9cb5227a3c28eb90e4a1f0330ff0db91138 100644
--- a/rbtools/commands/__init__.py
+++ b/rbtools/commands/__init__.py
@@ -61,6 +61,51 @@ class SmartHelpFormatter(argparse.HelpFormatter):
         return lines[:-1]
 
 
+class OutputWrapper(object):
+    """Wrapper for output of a command.
+
+    Wrapper around some output object that handles outputting messages.
+    Child classes specify the default object. The wrapper can handle
+    messages in either unicode or bytes.
+
+    Version Added:
+        4.0
+    """
+    def __init__(self, output_stream):
+        """Initialize with an output object to stream to.
+
+        Args:
+            output_stream (object):
+                The output stream to send command output to.
+        """
+        self.output_stream = output_stream
+
+    def write(self, msg, end=None):
+        """Write a message to the output stream.
+
+        Write a string to output stream object if defined, otherwise
+        do nothing. end specifies a string that should be appended to
+        the end of msg before being given to the output stream.
+
+        Args:
+            msg (unicode):
+                String to write to output stream.
+            end (unicode, optional):
+                String to append to end of msg. This defaults to ``None```.
+        """
+        if self.output_stream:
+            if end:
+                msg += end
+
+            self.output_stream.write(msg)
+
+    def new_line(self):
+        """Pass a new line character to output stream object.
+        """
+        if self.output_stream:
+            self.output_stream.write('\n')
+
+
 class Option(object):
     """Represents an option for a command.
 
@@ -166,6 +211,26 @@ class Command(object):
 
     ``option_list`` is a list of command line options for the command.
     Each list entry should be an Option or OptionGroup instance.
+
+    Attributes:
+        log (logging):
+            Logger for logging events.
+
+        transport_cls (SyncTransport):
+            synchronous transport layer for API client.
+
+        stdout (OutputWrapper):
+            Standard unicode output wrapper that subclasses must write to.
+
+        stderr (OutputWrapper):
+            Standard unicode error output wrapper that subclasses must write
+            to.
+
+        stdout_byte (OutputWrapper):
+            Standard byte output wrapper that subclasses must write to.
+
+        stderr_byte (OutputWrapper):
+            Standard byte error output wrapper that subclasses must write to.
     """
 
     name = ''
@@ -586,6 +651,16 @@ class Command(object):
         self.log = logging.getLogger('rb.%s' % self.name)
         self.transport_cls = transport_cls or self.default_transport_cls
 
+        self.stdout = OutputWrapper(sys.stdout)
+        self.stderr = OutputWrapper(sys.stderr)
+
+        if six.PY2:
+            self.stderr_bytes = OutputWrapper(sys.stderr)
+            self.stdout_bytes = OutputWrapper(sys.stdout)
+        else:
+            self.stderr_bytes = OutputWrapper(sys.stderr.buffer)
+            self.stdout_bytes = OutputWrapper(sys.stdout.buffer)
+
     def create_parser(self, config, argv=[]):
         """Create and return the argument parser for this command."""
         parser = argparse.ArgumentParser(
@@ -891,9 +966,10 @@ class Command(object):
 
                 raise CommandError('Unable to log in to Review Board.')
 
-            print()
-            print('Please log in to the Review Board server at %s.' %
-                  urlparse(uri)[1])
+            self.stdout.new_line()
+            self.stdout.write('Please log in to the Review Board server at '
+                              '%s.'
+                              % urlparse(uri)[1])
 
             if username is None:
                 username = get_input('Username: ')
@@ -915,22 +991,23 @@ class Command(object):
                                'required, but cannot be used with '
                                '--diff-filename=-')
 
-        print()
-        print('Please enter your two-factor authentication token for Review '
-              'Board.')
+        self.stdout.new_line()
+        self.stdout.write('Please enter your two-factor authentication '
+                          'token for Review Board.')
 
         if token_method == 'sms':
-            print('You should be getting a text message with '
-                  'an authentication token.')
-            print('Enter the token below.')
+            self.stdout.write('You should be getting a text message with '
+                              'an authentication token.')
+            self.stdout.write('Enter the token below.')
         elif token_method == 'call':
-            print('You should be getting an automated phone call with '
-                  'an authentication token.')
-            print('Enter the token below.')
+            self.stdout.write('You should be getting an automated phone '
+                              'call with an authentication token.')
+            self.stdout.write('Enter the token below.')
         elif token_method == 'generator':
-            print('Enter the token shown on your token generator app below.')
+            self.stdout.write('Enter the token shown on your token '
+                              'generator app below.')
 
-        print()
+        self.stdout.new_line()
 
         return get_pass('Token: ', require=True)
 
diff --git a/rbtools/commands/alias.py b/rbtools/commands/alias.py
index 086b02c3911b12eccb1797def140cc95033ceb4f..d8fd6220f5fccf9c876aafe4bdc9e149e456d20a 100644
--- a/rbtools/commands/alias.py
+++ b/rbtools/commands/alias.py
@@ -64,19 +64,20 @@ class Alias(Command):
 
         for config_path in config_paths:
             if aliases[config_path]:
-                print('[%s]' % config_path)
+                self.stdout.write('[%s]' % config_path)
 
                 for alias_name, entry in six.iteritems(aliases[config_path]):
-                    print('    %s = %s' % (alias_name, entry['command']))
+                    self.stdout.write('    %s = %s'
+                                      % (alias_name, entry['command']))
 
                     if entry['invalid']:
-                        print('      !! This alias is overridden by an rbt '
-                              'command !!')
+                        self.stdout.write('      !! This alias is overridden '
+                                          'by an rbt command !!')
                     elif entry['overridden']:
-                        print('      !! This alias is overridden by another '
-                              'alias in "%s" !!'
-                              % predefined_aliases[alias_name])
-                print()
+                        self.stdout.write('      !! This alias is overridden '
+                                          'by another alias in "%s" !!'
+                                          % predefined_aliases[alias_name])
+                self.stdout.new_line()
 
     def main(self, *args):
         """Run the command."""
@@ -96,4 +97,4 @@ class Alias(Command):
 
             command = expand_alias(alias, args)[0]
 
-            print(list2cmdline(command))
+            self.stdout.write(list2cmdline(command))
diff --git a/rbtools/commands/api_get.py b/rbtools/commands/api_get.py
index 7b8c51d5877c6ad112155f4652c0ad9ad108cb6f..99022644a1d005ebadad8da1c69b0b29818fa88a 100644
--- a/rbtools/commands/api_get.py
+++ b/rbtools/commands/api_get.py
@@ -59,10 +59,10 @@ class APIGet(Command):
                 resource = api_client.get_path(path, **query_args)
         except APIError as e:
             if e.rsp:
-                print(self._dumps(e.rsp))
+                self.stdout.write(self._dumps(e.rsp))
                 raise CommandExit(1)
             else:
                 raise CommandError('Could not retrieve the requested '
                                    'resource: %s' % e)
 
-        print(self._dumps(resource.rsp))
+        self.stdout.write(self._dumps(resource.rsp))
diff --git a/rbtools/commands/attach.py b/rbtools/commands/attach.py
index 5f4f3f5220665afd28d1db03f49265817a543b1e..0c8f2f0cf3e81edaad9118d152ab80528193907f 100644
--- a/rbtools/commands/attach.py
+++ b/rbtools/commands/attach.py
@@ -54,5 +54,5 @@ class Attach(Command):
         except APIError as e:
             raise CommandError('Error uploading file: %s' % e)
 
-        print('Uploaded %s to review request %s.' %
-              (path_to_file, review_request_id))
+        self.stdout.write('Uploaded %s to review request %s.'
+                          % (path_to_file, review_request_id))
diff --git a/rbtools/commands/close.py b/rbtools/commands/close.py
index 083c2d6b19b1ea8c7c17b3c084e5c6c410544b06..d21cc224dd299b43ac8b1461a06b600855c6af01 100644
--- a/rbtools/commands/close.py
+++ b/rbtools/commands/close.py
@@ -73,5 +73,5 @@ class Close(Command):
         else:
             review_request = review_request.update(status=close_type)
 
-        print('Review request #%s is set to %s.' %
-              (review_request_id, review_request.status))
+        self.stdout.write('Review request #%s is set to %s.'
+                          % (review_request_id, review_request.status))
diff --git a/rbtools/commands/diff.py b/rbtools/commands/diff.py
index 4d7ccd631b676dfc5ed49416792eba9d992337ee..36d13d3427c00d43715d35db0b32fcdb783a3f4d 100644
--- a/rbtools/commands/diff.py
+++ b/rbtools/commands/diff.py
@@ -92,8 +92,8 @@ class Diff(Command):
 
         if diff:
             if six.PY2:
-                print(diff)
+                self.stdout.write(diff)
             else:
                 # Write the non-decoded binary diff to standard out
-                sys.stdout.buffer.write(diff)
-                print()
+                self.stdout_byte.write(diff)
+                self.stdout.new_line()
diff --git a/rbtools/commands/info.py b/rbtools/commands/info.py
index 9e6e8c3987a166e92c3c15c436da78e4332766d1..0b7b54e035af9762a8cb11efa85355cd15dca759 100644
--- a/rbtools/commands/info.py
+++ b/rbtools/commands/info.py
@@ -58,27 +58,27 @@ class Info(Command):
             raise CommandError('This review request does not have diffs '
                                'attached')
 
-        print(review_request.summary)
-        print()
-        print('Submitter: %s'
-              % (review_request.submitter.fullname or
-                 review_request.submitter.username))
-        print()
-        print(review_request.description)
+        self.stdout.write(review_request.summary)
+        self.stdout.new_line()
+        self.stdout.write('Submitter: %s'
+                          % (review_request.submitter.fullname or
+                             review_request.submitter.username))
+        self.stdout.new_line()
+        self.stdout.write(review_request.description)
 
-        print()
-        print('URL: %s' % review_request.absolute_url)
+        self.stdout.new_line()
+        self.stdout.write('URL: %s' % review_request.absolute_url)
 
         if diff:
-            print ('Diff: %sdiff/%s/'
-                   % (review_request.absolute_url, diff_revision))
-            print()
-            print('Revision: %s (of %d)'
-                  % (diff_revision, diffs.total_results))
+            self.stdout.write('Diff: %sdiff/%s/'
+                              % (review_request.absolute_url, diff_revision))
+            self.stdout.new_line()
+            self.stdout.write('Revision: %s (of %d)'
+                              % (diff_revision, diffs.total_results))
 
             if commits:
-                print()
-                print('Commits:')
+                self.stdout.new_line()
+                self.stdout.write('Commits:')
 
                 table = Texttable(get_terminal_size().columns)
                 table.header(('ID', 'Summary', 'Author'))
@@ -92,4 +92,4 @@ class Info(Command):
                     table.add_row((commit.commit_id, summary,
                                    commit.author_name))
 
-                print(table.draw())
+                self.stdout.write(table.draw())
diff --git a/rbtools/commands/land.py b/rbtools/commands/land.py
index 6d04f746c118b574d986859316804c6681cde70f..e4d71173a047dda5d54333f30d5ef67dc0e986c1 100644
--- a/rbtools/commands/land.py
+++ b/rbtools/commands/land.py
@@ -224,11 +224,11 @@ class Land(Command):
             author = review_request.get_submitter()
 
             if squash:
-                print('Squashing branch "%s" into "%s".'
-                      % (source_branch, destination_branch))
+                self.stdout.write('Squashing branch "%s" into "%s".'
+                                  % (source_branch, destination_branch))
             else:
-                print('Merging branch "%s" into "%s".'
-                      % (source_branch, destination_branch))
+                self.stdout.write('Merging branch "%s" into "%s".'
+                                  % (source_branch, destination_branch))
 
             if not dry_run:
                 try:
@@ -242,14 +242,16 @@ class Land(Command):
                 except MergeError as e:
                     raise CommandError(six.text_type(e))
         else:
-            print('Applying patch from review request %s.' % review_request.id)
+            self.stdout.write('Applying patch from review request %s.'
+                              % review_request.id)
 
             if not dry_run:
                 self.patch(review_request.id,
                            squash=squash)
 
-        print('Review request %s has landed on "%s".' %
-              (review_request.id, self.options.destination_branch))
+        self.stdout.write('Review request %s has landed on "%s".'
+                          % (review_request.id,
+                             self.options.destination_branch))
 
     def main(self, branch_name=None, *args):
         """Run the command."""
@@ -361,8 +363,9 @@ class Land(Command):
             dependencies = toposort(dependency_graph)[1:]
 
             if dependencies:
-                print('Recursively landing dependencies of review request %s.'
-                      % review_request_id)
+                self.stdout.write('Recursively landing dependencies of '
+                                  'review request %s.'
+                                  % review_request_id)
 
                 for dependency in dependencies:
                     land_error = self.can_land(dependency)
@@ -381,8 +384,8 @@ class Land(Command):
                   **land_kwargs)
 
         if self.options.push:
-            print('Pushing branch "%s" upstream'
-                  % self.options.destination_branch)
+            self.stdout.write('Pushing branch "%s" upstream'
+                              % self.options.destination_branch)
 
             if not self.options.dry_run:
                 try:
diff --git a/rbtools/commands/patch.py b/rbtools/commands/patch.py
index fa2358a02ccf1e0b5e0e91d28f92314fb78c20e7..3c1ab2a95c773da5bed865673f23ffe9c84aec22 100644
--- a/rbtools/commands/patch.py
+++ b/rbtools/commands/patch.py
@@ -342,17 +342,17 @@ class Patch(Command):
             revert=revert)
 
         if result.patch_output:
-            print()
+            self.stdout.new_line()
 
             patch_output = result.patch_output.strip()
 
             if six.PY2:
-                print(patch_output)
+                self.stdout.write(patch_output)
             else:
-                sys.stdout.buffer.write(patch_output)
-                print()
+                self.stdout_bytes.write(patch_output)
+                self.stdout.new_line()
 
-            print()
+            self.stdout.new_line()
 
         if not result.applied:
             if revert:
@@ -367,24 +367,24 @@ class Patch(Command):
         if result.has_conflicts:
             if result.conflicting_files:
                 if revert:
-                    print('The patch was partially reverted, but there were '
-                          'conflicts in:')
+                    self.stdout.write('The patch was partially reverted, but '
+                                      'there were conflicts in:')
                 else:
-                    print('The patch was partially applied, but there were '
-                          'conflicts in:')
+                    self.stdout.write('The patch was partially applied, but '
+                                      'there were conflicts in:')
 
-                print()
+                self.stdout.new_line()
 
                 for filename in result.conflicting_files:
-                    print('    %s' % filename)
+                    self.stdout.write('    %s' % filename)
 
-                print()
+                self.stdout.new_line()
             elif revert:
-                print('The patch was partially reverted, but there were '
-                      'conflicts.')
+                self.stdout.write('The patch was partially reverted, '
+                                  'but there were conflicts.')
             else:
-                print('The patch was partially applied, but there were '
-                      'conflicts.')
+                self.stdout.write('The patch was partially applied, but '
+                                  'there were conflicts.')
 
             return False
 
@@ -494,13 +494,10 @@ class Patch(Command):
             diff_body = patch_data['diff']
 
             if isinstance(diff_body, bytes):
-                if six.PY3:
-                    sys.stdout.buffer.write(diff_body)
-                    print()
-                else:
-                    print(diff_body.decode('utf-8'))
+                self.stdout_bytes.write(diff_body)
+                self.stdout.new_line()
             else:
-                print(diff_body)
+                self.stdout.write(diff_body)
 
     def _apply_patches(self, patches):
         """Apply a list of patches to the tree.
diff --git a/rbtools/commands/post.py b/rbtools/commands/post.py
index 06afa28572b39f48451ebb4af1056814209a62dd..5cd24ea457cda0f7701044cae626da6aa245c9ae 100644
--- a/rbtools/commands/post.py
+++ b/rbtools/commands/post.py
@@ -754,27 +754,30 @@ class Post(Command):
         # upon posting.
         if self.options.stamp_when_posting:
             if diff_history:
-                print('Cannot stamp review request URL when posting with '
-                      'history.')
+                self.stdout.write('Cannot stamp review request URL '
+                                  'when posting with history.')
             elif not self.tool.can_amend_commit:
-                print('Cannot stamp review rquest URL onto the commit '
-                      'message; stamping is not supported with %s.'
-                      % self.tool.name)
+                self.stdout.write('Cannot stamp review request URL '
+                                  'onto the commit message; stamping is '
+                                  'not supported with %s.'
+                                  % self.tool.name)
 
             else:
                 try:
                     stamp_commit_with_review_url(self.revisions,
                                                  review_request.absolute_url,
                                                  self.tool)
-                    print('Stamped review URL onto the commit message.')
+                    self.stdout.write('Stamped review URL onto the '
+                                      'commit message.')
                 except AlreadyStampedError:
-                    print('Commit message has already been stamped')
+                    self.stdout.write('Commit message has already been '
+                                      'stamped')
                 except Exception as e:
                     logging.debug('Caught exception while stamping the '
                                   'commit message. Proceeding to post '
                                   'without stamping.', exc_info=True)
-                    print('Could not stamp review request URL onto the commit '
-                          'message.')
+                    self.stdout.write('Could not stamp review request URL '
+                                      'onto the commit message.')
 
         # Update the review request draft fields based on options set
         # by the user, or configuration.
@@ -1102,10 +1105,10 @@ class Post(Command):
             squashed_diff=squashed_diff,
             submit_as=self.options.submit_as)
 
-        print('Review request #%s posted.' % review_request_id)
-        print()
-        print(review_request_url)
-        print('%sdiff/' % review_request_url)
+        self.stdout.write('Review request #%s posted.' % review_request_id)
+        self.stdout.new_line()
+        self.stdout.write(review_request_url)
+        self.stdout.write('%sdiff/' % review_request_url)
 
         # Load the review up in the browser if requested to.
         if self.options.open_browser:
diff --git a/rbtools/commands/publish.py b/rbtools/commands/publish.py
index 7398e48ef8db28b1b36644c6f884e098bdb23962..068fda4d392777c25805d83f4d84808f63211be7 100644
--- a/rbtools/commands/publish.py
+++ b/rbtools/commands/publish.py
@@ -86,4 +86,5 @@ class Publish(Command):
             raise CommandError('Error publishing review request (it may '
                                'already be published): %s' % e)
 
-        print('Review request #%s is published.' % review_request_id)
+        self.stdout.write('Review request #%s is published.'
+                          % review_request_id)
diff --git a/rbtools/commands/setup_completion.py b/rbtools/commands/setup_completion.py
index 397da2e71e2b8dc140e7e92488b68c5f42b2f7e6..58808e1f3d08c5d5115bc17601df92e4a9d3281f 100644
--- a/rbtools/commands/setup_completion.py
+++ b/rbtools/commands/setup_completion.py
@@ -74,8 +74,9 @@ class SetupCompletion(Command):
             logging.error('I/O Error (%s): %s', e.errno, e.strerror)
             sys.exit()
 
-        print('Successfully installed %s auto-completions.' % shell)
-        print('Restart the terminal for completions to work.')
+        self.stdout.write('Successfully installed %s auto-completions.'
+                          % shell)
+        self.stdout.write('Restart the terminal for completions to work.')
 
     def main(self, shell=None):
         """Run the command.
diff --git a/rbtools/commands/setup_repo.py b/rbtools/commands/setup_repo.py
index edbebe65c26ad2ffe32b60eaefd2b5f3a99d6c52..5665f697dc5f31c20361b1326a88a797dd9e9823 100644
--- a/rbtools/commands/setup_repo.py
+++ b/rbtools/commands/setup_repo.py
@@ -64,8 +64,8 @@ class SetupRepo(Command):
                                                   n=4, cutoff=0.4)
 
         if closest_paths:
-            print()
-            print(
+            self.stdout.new_line()
+            self.stdout.write(
                 '%(num)s %(repo_type)s repositories found:'
                 % {
                     'num': len(closest_paths),
@@ -80,10 +80,11 @@ class SetupRepo(Command):
                 current_repo_index = closest_paths[repo_chosen]
                 current_repo = repo_paths[current_repo_index]
 
-                print()
-                print('Selecting "%s" (%s)...' % (current_repo['name'],
-                                                  current_repo['path']))
-                print()
+                self.stdout.new_line()
+                self.stdout.write('Selecting "%s" (%s)...'
+                                  % (current_repo['name'],
+                                     current_repo['path']))
+                self.stdout.new_line()
 
                 return current_repo
 
@@ -110,8 +111,8 @@ class SetupRepo(Command):
             raise CommandError('I/O error generating config file (%s): %s'
                                % (e.errno, e.strerror))
 
-        print('%s creation successful! Config written to %s' % (CONFIG_FILE,
-                                                                file_path))
+        self.stdout.write('%s creation successful! Config written to %s'
+                          % (CONFIG_FILE, file_path))
 
     def main(self, *args):
         server = self.options.server
@@ -119,26 +120,27 @@ class SetupRepo(Command):
         api_root = None
 
         if not server:
-            print()
-            print(textwrap.fill(
-                'This command is intended to help users create a %s file in '
-                'the current directory to connect a repository and Review '
-                'Board server.')
-                % CONFIG_FILE)
-            print()
-            print(textwrap.fill(
-                'Repositories must currently exist on your server (either '
-                'hosted internally or via RBCommons) to successfully '
-                'generate this file.'))
-            print(textwrap.fill(
-                'Repositories can be added using the Admin Dashboard in '
-                'Review Board or under your team administration settings in '
-                'RBCommons.'))
-            print()
-            print(textwrap.fill(
-                'Press CTRL + C anytime during this command to cancel '
-                'generating your config file.'))
-            print()
+            self.stdout.new_line()
+            self.stdout.write(textwrap.fill(
+                              'This command is intended to help users create '
+                              'a %s file in the current directory to connect '
+                              'a repository and Review Board server.')
+                              % CONFIG_FILE)
+            self.stdout.new_line()
+            self.stdout.write(textwrap.fill(
+                              'Repositories must currently exist on your '
+                              'server (either hosted internally or via '
+                              'RBCommons) to successfully generate this '
+                              'file.'))
+            self.stdout.write(textwrap.fill(
+                              'Repositories can be added using the Admin ',
+                              'Dashboard in Review Board or under your '
+                              'team administration settings in RBCommons.'))
+            self.stdout.new_line()
+            self.stdout.write(textwrap.fill(
+                              'Press CTRL + C anytime during this command '
+                              'to cancel generating your config file.'))
+            self.stdout.new_line()
 
             while True:
                 server = input('Enter the Review Board server URL: ')
@@ -149,10 +151,10 @@ class SetupRepo(Command):
                         api_client, api_root = self.get_api(server)
                         break
                     except CommandError as e:
-                        print()
-                        print('%s' % e)
-                        print('Please try again.')
-                        print()
+                        self.stdout.new_line()
+                        self.stdout.write('%s' % e)
+                        self.stdout.write('Please try again.')
+                        self.stdout.new_line()
 
         repository_info, tool = self.initialize_scm_tool()
 
@@ -171,25 +173,28 @@ class SetupRepo(Command):
         # While a repository is not chosen, keep the repository selection
         # prompt displayed until the prompt is cancelled.
         while True:
-            print()
-            print('Current server: %s' % server)
+            self.stdout.new_line()
+            self.stdout.write('Current server: %s' % server)
             selected_repo = self.prompt_rb_repository(
                 tool.name, repository_info, api_root)
 
             if not selected_repo:
-                print()
-                print('No %s repository found for the Review Board server %s'
-                      % (tool.name, server))
-                print()
-                print('Cancelling %s creation...' % CONFIG_FILE)
-                print()
-                print(textwrap.fill(
-                    'Please make sure your repositories currently exist on '
-                    'your server. Repositories can be configured using the '
-                    'Review Board Admin Dashboard or under your team '
-                    'administration settings in RBCommons. For more '
-                    'information, see `rbt help setup-repo` or the official '
-                    'docs at https://www.reviewboard.org/docs/.'))
+                self.stdout.new_line()
+                self.stdout.write('No %s repository found for the Review '
+                                  'Board server %s'
+                                  % (tool.name, server))
+                self.stdout.new_line()
+                self.stdout.write('Cancelling %s creation...' % CONFIG_FILE)
+                self.stdout.new_line()
+                self.stdout.write(textwrap.fill(
+                    'Please make sure your repositories '
+                    'currently exist on your server. '
+                    'Repositories can be configured using the '
+                    'Review Board Admin Dashboard or under your '
+                    'team administration settings in RBCommons. '
+                    'For more information, see `rbt help '
+                    'setup-repo` or the official docs at '
+                    'https://www.reviewboard.org/docs/.'))
                 return
 
             config = [
@@ -235,7 +240,7 @@ class SetupRepo(Command):
         """
         for i, repo_url in enumerate(closest_paths):
             repo = repo_paths[repo_url]
-            print(
+            self.stdout.write(
                 '%(num)d) "%(repo_name)s" (%(repo_url)s)'
                 % {
                     'num': i + 1,
diff --git a/rbtools/commands/stamp.py b/rbtools/commands/stamp.py
index 1967db5c3aa40ae7ea99305426645d287f425002..2d7e750e71ea3455effd86922826c6afe48d182d 100644
--- a/rbtools/commands/stamp.py
+++ b/rbtools/commands/stamp.py
@@ -150,5 +150,5 @@ class Stamp(Command):
 
         stamp_commit_with_review_url(revisions, review_request_url, self.tool)
 
-        print('Successfully stamped change with the URL:')
-        print(review_request_url)
+        self.stdout.write('Successfully stamped change with the URL:')
+        self.stdout.write(review_request_url)
diff --git a/rbtools/commands/status.py b/rbtools/commands/status.py
index c39a7b51bd38c2fb5687eb9058aa3cd5241a3720..1a446c67093eecce02ccb190a37b9865facb5a93 100644
--- a/rbtools/commands/status.py
+++ b/rbtools/commands/status.py
@@ -102,11 +102,11 @@ class Status(Command):
 
                 table.add_row(row)
 
-            print(table.draw())
+            self.stdout.write(table.draw())
         else:
-            print('No review requests found.')
+            self.stdout.write('No review requests found.')
 
-        print()
+        self.stdout.new_line()
 
     def get_data(self, requests):
         """Return current status and review summary for all reviews.
@@ -210,4 +210,4 @@ class Status(Command):
             end = '\n'
 
         for info in review_requests:
-            print(fmt % info, end=end)
+            self.stdout.write(fmt % info, end=end)
diff --git a/rbtools/commands/status_update.py b/rbtools/commands/status_update.py
index 34084ea6af0db5da5c06795f49bd754019ce49c7..a9117c28dea7c3d4e95386299a5da38cf329f152 100644
--- a/rbtools/commands/status_update.py
+++ b/rbtools/commands/status_update.py
@@ -127,10 +127,12 @@ class StatusUpdate(Command):
         else:
             description = ''
 
-        print(' %d\t%s: <%s> %s%s' %
-              (status_update.get('id'), status_update.get('service_id'),
-               status_update.get('state'), status_update.get('summary'),
-               description))
+        self.stdout.write(' %d\t%s: <%s> %s%s'
+                          % (status_update.get('id'),
+                             status_update.get('service_id'),
+                             status_update.get('state'),
+                             status_update.get('summary'),
+                             description))
 
     def _dict_status_update(self, status_update):
         """Create a dict for status update.
@@ -168,7 +170,7 @@ class StatusUpdate(Command):
             else:
                 output = self._dict_status_update(response)
 
-            print(json.dumps(output, indent=2, sort_keys=True))
+            self.stdout.write(json.dumps(output, indent=2, sort_keys=True))
         else:
             if isinstance(response, list):
                 for status_update in response:
@@ -308,7 +310,7 @@ class StatusUpdate(Command):
                     .rsp.get('status_updates'))
         except APIError as e:
             if e.rsp:
-                print(json.dumps(e.rsp, indent=2))
+                self.stdout.write(json.dumps(e.rsp, indent=2))
                 raise CommandExit(1)
             else:
                 raise CommandError('Could not retrieve the requested '
@@ -386,7 +388,7 @@ class StatusUpdate(Command):
             self.print(status_update.rsp.get('status_update'))
         except APIError as e:
             if e.rsp:
-                print(json.dumps(e.rsp, indent=2))
+                self.stdout.write(json.dumps(e.rsp, indent=2))
                 raise CommandExit(1)
             else:
                 raise CommandError('Could not set the requested '
diff --git a/rbtools/commands/tests/test_main.py b/rbtools/commands/tests/test_main.py
index 8dae63f592727e42bbf371dbe50e7e3e4cb077d3..fb4096d5484f256cf71a25a1a8a6e1d2ae8019ed 100644
--- a/rbtools/commands/tests/test_main.py
+++ b/rbtools/commands/tests/test_main.py
@@ -5,8 +5,10 @@ from __future__ import unicode_literals
 import os.path
 import sys
 
+import kgb
+
 from rbtools import get_version_string
-from rbtools.commands import main as rbt_main
+from rbtools.commands import main as rbt_main, OutputWrapper
 from rbtools.utils.process import execute
 from rbtools.utils.testbase import RBTestBase
 
@@ -118,3 +120,36 @@ class MainCommandTests(RBTestBase):
             The resulting output from the command.
         """
         return execute([sys.executable, _rbt_path] + list(args))
+
+
+class OutputWrapperTests(kgb.SpyAgency, RBTestBase):
+    """Unit tests for command OutputWrapper."""
+
+    def test_output_wrapper_initiates(self):
+        """Testing OutputWrapper instantiates given stream object
+        """
+        stdout = OutputWrapper(sys.stdout)
+
+        self.assertIs(stdout.output_stream, sys.stdout)
+
+    def test_output_wrapper_write(self):
+        """Testing OutputWrapper.write passes correct message to stream object
+        """
+        stdout = OutputWrapper(sys.stdout)
+
+        self.spy_on(sys.stdout.write)
+        stdout.write('test')
+        self.assertSpyCalledWith(sys.stdout.write, 'test')
+
+        stdout.write(msg='test', end='end')
+        self.assertSpyCalledWith(sys.stdout.write, 'testend')
+
+    def test_output_wrapper_newline(self):
+        """Testing OutputWrapper.new_line passes a newline character to
+        stream object
+        """
+        stdout = OutputWrapper(sys.stdout)
+
+        self.spy_on(sys.stdout.write)
+        stdout.new_line()
+        self.assertSpyCalledWith(sys.stdout.write, '\n')
