← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:reduce-ensure-str into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:reduce-ensure-str into launchpad:master.

Commit message:
Remove simple uses of six.ensure_str

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/415902

These were clearly no-ops on Python 3, but missed by pyupgrade for various reasons, such as being in doctests.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:reduce-ensure-str into launchpad:master.
diff --git a/lib/launchpad_loggerhead/app.py b/lib/launchpad_loggerhead/app.py
index 7ada98a..3e693f2 100644
--- a/lib/launchpad_loggerhead/app.py
+++ b/lib/launchpad_loggerhead/app.py
@@ -40,7 +40,6 @@ from paste.request import (
     parse_querystring,
     path_info_pop,
     )
-import six
 from six.moves.urllib.parse import (
     urlencode,
     urljoin,
@@ -256,7 +255,7 @@ class RootApp:
             self.log.info('Using branch: %s', branch_name)
             if trail and not trail.startswith('/'):
                 trail = '/' + trail
-            environ['PATH_INFO'] = six.ensure_str(trail)
+            environ['PATH_INFO'] = trail
             environ['SCRIPT_NAME'] += consumed.rstrip('/')
             branch_url = lp_server.get_url() + branch_name
             branch_link = urljoin(
diff --git a/lib/lp/answers/stories/question-obfuscation.txt b/lib/lp/answers/stories/question-obfuscation.txt
index f6a23c9..1f082fd 100644
--- a/lib/lp/answers/stories/question-obfuscation.txt
+++ b/lib/lp/answers/stories/question-obfuscation.txt
@@ -96,7 +96,7 @@ Anonymous cannot see the email address anywhere on the Answers front
 page.
 
     >>> anon_browser.open('http://answers.launchpad.test/')
-    >>> six.ensure_str('user@xxxxxxxxxx') in anon_browser.contents
+    >>> 'user@xxxxxxxxxx' in anon_browser.contents
     False
 
     >>> question_portlet = find_tag_by_id(
@@ -132,7 +132,7 @@ They cannot see the address reading the question either.
     link ...
 
     >>> anon_browser.getLink('mailto: problem in webpage').click()
-    >>> six.ensure_str('user@xxxxxxxxxx') in anon_browser.contents
+    >>> 'user@xxxxxxxxxx' in anon_browser.contents
     False
 
     >>> description = find_main_content(anon_browser.contents).p
diff --git a/lib/lp/app/doc/displaying-paragraphs-of-text.txt b/lib/lp/app/doc/displaying-paragraphs-of-text.txt
index 94171e0..c50955a 100644
--- a/lib/lp/app/doc/displaying-paragraphs-of-text.txt
+++ b/lib/lp/app/doc/displaying-paragraphs-of-text.txt
@@ -494,7 +494,7 @@ we want to replace a variable number of spaces with the same number of
 
     >>> from lp.app.browser.stringformatter import FormattersAPI
     >>> import re
-    >>> matchobj = re.match('foo(.*)bar', six.ensure_str('fooX Ybar'))
+    >>> matchobj = re.match('foo(.*)bar', 'fooX Ybar')
     >>> matchobj.groups()
     ('X Y',)
     >>> FormattersAPI._substitute_matchgroup_for_spaces(matchobj)
@@ -508,10 +508,9 @@ First, let's try a match of nothing it understands.  This is a bug, so we get
 an AssertionError.
 
     >>> matchobj = re.match(
-    ...     six.ensure_str(
-    ...         '(?P<bug>xxx)?(?P<faq>www)?(?P<url>yyy)?(?P<oops>zzz)?'
-    ...         '(?P<lpbranchurl>www)?(?P<clbug>vvv)?'),
-    ...     six.ensure_str('fish'))
+    ...     '(?P<bug>xxx)?(?P<faq>www)?(?P<url>yyy)?(?P<oops>zzz)?'
+    ...     '(?P<lpbranchurl>www)?(?P<clbug>vvv)?',
+    ...     'fish')
     >>> sorted(matchobj.groupdict().items())
     [('bug', None),
      ('clbug', None),
@@ -527,9 +526,7 @@ an AssertionError.
 When we have a URL, the URL is made into a link.  A quote is added to the
 url to demonstrate quoting in the HTML attribute.
 
-    >>> matchobj = re.match(
-    ...     six.ensure_str('(?P<bug>xxx)?(?P<url>y"y)?'),
-    ...     six.ensure_str('y"y'))
+    >>> matchobj = re.match('(?P<bug>xxx)?(?P<url>y"y)?', 'y"y')
     >>> sorted(matchobj.groupdict().items())
     [('bug', None), ('url', 'y"y')]
     >>> print(FormattersAPI._linkify_substitution(matchobj))
@@ -539,8 +536,7 @@ When we have a bug reference, the 'bug' group is used as the text of the link,
 and the 'bugnum' is used to look up the bug.
 
     >>> matchobj = re.match(
-    ...     six.ensure_str('(?P<bug>xxxx)?(?P<bugnum>2)?(?P<url>yyy)?'),
-    ...     six.ensure_str('xxxx2'))
+    ...     '(?P<bug>xxxx)?(?P<bugnum>2)?(?P<url>yyy)?', 'xxxx2')
     >>> sorted(matchobj.groupdict().items())
     [('bug', 'xxxx'), ('bugnum', '2'), ('url', None)]
     >>> FormattersAPI._linkify_substitution(matchobj)
@@ -550,8 +546,7 @@ When the bugnum doesn't match any bug, we still get a link, but get a message
 in the link's title.
 
     >>> matchobj = re.match(
-    ...     six.ensure_str('(?P<bug>xxxx)?(?P<bugnum>2000)?(?P<url>yyy)?'),
-    ...     six.ensure_str('xxxx2000'))
+    ...     '(?P<bug>xxxx)?(?P<bugnum>2000)?(?P<url>yyy)?', 'xxxx2000')
     >>> sorted(matchobj.groupdict().items())
     [('bug', 'xxxx'), ('bugnum', '2000'), ('url', None)]
     >>> FormattersAPI._linkify_substitution(matchobj)
diff --git a/lib/lp/app/doc/launchpadform.txt b/lib/lp/app/doc/launchpadform.txt
index db59937..6fa4e8e 100644
--- a/lib/lp/app/doc/launchpadform.txt
+++ b/lib/lp/app/doc/launchpadform.txt
@@ -229,7 +229,7 @@ setFieldError() method (for errors specific to a field):
     ...             self.addError('your password may not be the same '
     ...                           'as your name')
     ...         if data.get('password') == 'password':
-    ...             self.setFieldError(six.ensure_str('password'),
+    ...             self.setFieldError('password',
     ...                                'your password must not be "password"')
 
     >>> context = FormTest()
diff --git a/lib/lp/app/doc/launchpadview.txt b/lib/lp/app/doc/launchpadview.txt
index f49c1ef..2951085 100644
--- a/lib/lp/app/doc/launchpadview.txt
+++ b/lib/lp/app/doc/launchpadview.txt
@@ -62,7 +62,7 @@ an IStructuredString implementation.
     >>> print(view.info_message)
     None
 
-    >>> view.error_message = six.ensure_str('A simple string.')
+    >>> view.error_message = 'A simple string.'
     Traceback (most recent call last):
     ...
     ValueError: <... 'str'> is not a valid value for error_message,
@@ -70,7 +70,7 @@ an IStructuredString implementation.
     >>> print(view.error_message)
     None
 
-    >>> view.info_message = six.ensure_str('A simple string.')
+    >>> view.info_message = 'A simple string.'
     Traceback (most recent call last):
     ...
     ValueError: <... 'str'> is not a valid value for info_message,
diff --git a/lib/lp/app/doc/menus.txt b/lib/lp/app/doc/menus.txt
index c76ac01..001eac8 100644
--- a/lib/lp/app/doc/menus.txt
+++ b/lib/lp/app/doc/menus.txt
@@ -33,7 +33,7 @@ later, implementations having facets and menus will be defined.
 
     >>> import sys
     >>> import types
-    >>> cookingexample = types.ModuleType(six.ensure_str('cookingexample'))
+    >>> cookingexample = types.ModuleType('cookingexample')
     >>> sys.modules['lp.app.cookingexample'] = cookingexample
 
     >>> cookingexample.ICookbook = ICookbook
diff --git a/lib/lp/app/doc/textformatting.txt b/lib/lp/app/doc/textformatting.txt
index 3d9dd40..faee73c 100644
--- a/lib/lp/app/doc/textformatting.txt
+++ b/lib/lp/app/doc/textformatting.txt
@@ -230,14 +230,14 @@ The line endings are normalized to \n, so if we get a text with
 dos-style line endings, we get the following result:
 
     >>> mailwrapper = MailWrapper(width=56)
-    >>> dos_style_comment = six.ensure_str(
+    >>> dos_style_comment = (
     ...     "This paragraph is longer than 56 characters, so it should"
     ...     " be wrapped even though the paragraphs are separated with"
     ...     " dos-style line endings."
     ...     "\r\n\r\n"
     ...     "Here's the second paragraph.")
     >>> wrapped_text = mailwrapper.format(dos_style_comment)
-    >>> wrapped_text.split(six.ensure_str('\n'))
+    >>> wrapped_text.split('\n')
     ['This paragraph is longer than 56 characters, so it',
      'should be wrapped even though the paragraphs are',
      'separated with dos-style line endings.',
diff --git a/lib/lp/app/widgets/doc/announcement-date-widget.txt b/lib/lp/app/widgets/doc/announcement-date-widget.txt
index d90250c..3b901ce 100644
--- a/lib/lp/app/widgets/doc/announcement-date-widget.txt
+++ b/lib/lp/app/widgets/doc/announcement-date-widget.txt
@@ -9,7 +9,7 @@ future, or to manually publish it later.
     >>> from lp.testing.pages import extract_text
     >>> from lp.services.webapp.servers import LaunchpadTestRequest
     >>> from lp.app.widgets.announcementdate import AnnouncementDateWidget
-    >>> field = Field(__name__=six.ensure_str('foo'), title=u'Foo')
+    >>> field = Field(__name__='foo', title='Foo')
     >>> widget = AnnouncementDateWidget(field, LaunchpadTestRequest())
     >>> print(extract_text(widget()))
     Publish this announcement:
diff --git a/lib/lp/app/widgets/doc/noneable-text-widgets.txt b/lib/lp/app/widgets/doc/noneable-text-widgets.txt
index 85ca174..6f79a1c 100644
--- a/lib/lp/app/widgets/doc/noneable-text-widgets.txt
+++ b/lib/lp/app/widgets/doc/noneable-text-widgets.txt
@@ -65,5 +65,5 @@ Excess whitespace is stripped, but newlines are preserved.
     >>> request = LaunchpadTestRequest(
     ...     form={'field.summary' : ' flower \n grass '})
     >>> widget = NoneableDescriptionWidget(field, request)
-    >>> six.ensure_str(widget.getInputValue())
+    >>> widget.getInputValue()
     'flower \n grass'
diff --git a/lib/lp/app/widgets/doc/project-scope-widget.txt b/lib/lp/app/widgets/doc/project-scope-widget.txt
index 366a4bd..1871412 100644
--- a/lib/lp/app/widgets/doc/project-scope-widget.txt
+++ b/lib/lp/app/widgets/doc/project-scope-widget.txt
@@ -15,8 +15,7 @@ selected.
 
     >>> empty_request = LaunchpadTestRequest()
     >>> scope_field = Choice(
-    ...     __name__=six.ensure_str('scope'), vocabulary='ProjectGroup',
-    ...     required=False)
+    ...     __name__='scope', vocabulary='ProjectGroup', required=False)
     >>> scope_field = scope_field.bind(object())
     >>> widget = ProjectScopeWidget(
     ...     scope_field, scope_field.vocabulary, empty_request)
diff --git a/lib/lp/app/widgets/doc/stripped-text-widget.txt b/lib/lp/app/widgets/doc/stripped-text-widget.txt
index 3343cd7..c4612d1 100644
--- a/lib/lp/app/widgets/doc/stripped-text-widget.txt
+++ b/lib/lp/app/widgets/doc/stripped-text-widget.txt
@@ -57,7 +57,7 @@ provided.
     True
 
     >>> required_field = StrippedTextLine(
-    ...     __name__=six.ensure_str('field'), title=u'Title', required=True)
+    ...     __name__='field', title=u'Title', required=True)
     >>> request = LaunchpadTestRequest(form={'field.field':'    \n    '})
     >>> widget = StrippedTextWidget(required_field, request)
     >>> widget.getInputValue()
diff --git a/lib/lp/blueprints/browser/sprint.py b/lib/lp/blueprints/browser/sprint.py
index 84ac65e..95bf105 100644
--- a/lib/lp/blueprints/browser/sprint.py
+++ b/lib/lp/blueprints/browser/sprint.py
@@ -612,13 +612,6 @@ class SprintAttendeesCsvExportView(LaunchpadView):
                  attendance.time_starts.strftime('%Y-%m-%dT%H:%M:%SZ'),
                  attendance.time_ends.strftime('%Y-%m-%dT%H:%M:%SZ'),
                  str(attendance.is_physical)))
-        # On Python 2, CSV can't handle unicode, so we encode everything as
-        # UTF-8.  Note that none of the columns constructed above can ever
-        # be None, so it's safe to use six.ensure_str here (which will raise
-        # TypeError if given None).
-        rows = [[six.ensure_str(column)
-                 for column in row]
-                for row in rows]
         self.request.response.setHeader(
             'Content-type', 'text/csv;charset="utf-8"')
         self.request.response.setHeader(
diff --git a/lib/lp/blueprints/stories/blueprints/xx-editing.txt b/lib/lp/blueprints/stories/blueprints/xx-editing.txt
index b5b3e2c..fd1a89a 100644
--- a/lib/lp/blueprints/stories/blueprints/xx-editing.txt
+++ b/lib/lp/blueprints/stories/blueprints/xx-editing.txt
@@ -26,7 +26,7 @@ Launchpad won't let us use an URL already used in another blueprint.
     >>> browser.getControl('Specification URL').value = url
     >>> browser.getControl('Change').click()
 
-    >>> message = six.ensure_str(
+    >>> message = (
     ...     'https://wiki.ubuntu.com/MediaIntegrityCheck is already '
     ...     'registered by')
     >>> message in browser.contents
@@ -40,7 +40,7 @@ product.
     >>> browser.getControl('Specification URL').value = url
     >>> browser.getControl('Change').click()
 
-    >>> message = six.ensure_str('e4x is already in use by another blueprint')
+    >>> message = 'e4x is already in use by another blueprint'
     >>> message in browser.contents
     True
 
diff --git a/lib/lp/blueprints/stories/blueprints/xx-productseries.txt b/lib/lp/blueprints/stories/blueprints/xx-productseries.txt
index 309a17f..4b3f4a3 100644
--- a/lib/lp/blueprints/stories/blueprints/xx-productseries.txt
+++ b/lib/lp/blueprints/stories/blueprints/xx-productseries.txt
@@ -153,8 +153,7 @@ there are none left in the queue.
     ...     'http://blueprints.launchpad.test/firefox/1.0/+setgoals')
     >>> driver_browser.getControl('Support E4X').selected = True
     >>> driver_browser.getControl('Decline').click()
-    >>> six.ensure_str('Declined 1 specification(s)') in (
-    ...     driver_browser.contents)
+    >>> 'Declined 1 specification(s)' in driver_browser.contents
     True
 
 The accepted item should show up in the list of specs for this series:
@@ -216,9 +215,9 @@ OK, lets see if it was immediately accepted:
 
     >>> anon_browser.open(
     ...     'http://launchpad.test/firefox/+spec/svg-support')
-    >>> six.ensure_str('firefox/trunk') in anon_browser.contents
+    >>> 'firefox/trunk' in anon_browser.contents
     True
-    >>> six.ensure_str('Accepted') in anon_browser.contents
+    >>> 'Accepted' in anon_browser.contents
     True
 
 And lets put it back:
@@ -258,7 +257,7 @@ And again, it should be accepted automatically.
 
     >>> anon_browser.open(
     ...     'http://launchpad.test/firefox/+spec/svg-support')
-    >>> six.ensure_str('firefox/1.0') in anon_browser.contents
+    >>> 'firefox/1.0' in anon_browser.contents
     True
-    >>> six.ensure_str('Accepted') in anon_browser.contents
+    >>> 'Accepted' in anon_browser.contents
     True
diff --git a/lib/lp/blueprints/stories/standalone/subscribing.txt b/lib/lp/blueprints/stories/standalone/subscribing.txt
index beab847..f8289a0 100644
--- a/lib/lp/blueprints/stories/standalone/subscribing.txt
+++ b/lib/lp/blueprints/stories/standalone/subscribing.txt
@@ -52,11 +52,10 @@ Now, we'll POST the form. We should see a message that we have just
 subscribed.
 
     >>> browser.getControl('Subscribe').click()
-    >>> six.ensure_str('You have subscribed to this blueprint') in (
-    ...     browser.contents)
+    >>> 'You have subscribed to this blueprint' in browser.contents
     True
 
-    >>> six.ensure_str('subscriber-essential') in browser.contents
+    >>> 'subscriber-essential' in browser.contents
     True
 
 Now the link should say "Update subscription" in the actions menu.
@@ -77,11 +76,10 @@ We will unset the essential flag and resubmit:
 
     >>> essential.selected = False
     >>> browser.getControl('Change').click()
-    >>> six.ensure_str('Your subscription has been updated') in (
-    ...     browser.contents)
+    >>> 'Your subscription has been updated' in browser.contents
     True
 
-    >>> six.ensure_str('subscriber-inessential') in browser.contents
+    >>> 'subscriber-inessential' in browser.contents
     True
 
 It's also possible to change the essential flag clicking on the star
@@ -93,7 +91,7 @@ icon in the Subscribers portlet.
     >>> browser.url
     'http://blueprints.launchpad.test/firefox/+spec/e4x'
 
-    >>> six.ensure_str('subscriber-essential') in browser.contents
+    >>> 'subscriber-essential' in browser.contents
     True
 
 We don't really want to be subscribed, so lets unsubscribe from that
@@ -104,14 +102,13 @@ unsubscribe confirmation page loads.
     >>> unsubit.click()
     >>> confirm = browser.getControl('Unsubscribe')
     >>> confirm.click()
-    >>> six.ensure_str('You have unsubscribed from this blueprint.') in (
-    ...     browser.contents)
+    >>> 'You have unsubscribed from this blueprint.' in browser.contents
     True
 
-    >>> six.ensure_str('subscriber-inessential') in browser.contents
+    >>> 'subscriber-inessential' in browser.contents
     False
 
-    >>> six.ensure_str('subscriber-essential') in browser.contents
+    >>> 'subscriber-essential' in browser.contents
     False
 
 
@@ -128,8 +125,7 @@ When we want other users to track a specification we can subscribe them.
 
     >>> browser.getControl('Subscriber').value = 'stub'
     >>> browser.getControl('Subscribe').click()
-    >>> msg = six.ensure_str(
-    ...     "Stuart Bishop has been subscribed to this blueprint.")
+    >>> msg = "Stuart Bishop has been subscribed to this blueprint."
     >>> msg in browser.contents
     True
 
@@ -280,7 +276,7 @@ list, and now the unsubscribe confirmation page loads.
     >>> unsubit.click()
     >>> confirm = browser.getControl('Unsubscribe')
     >>> confirm.click()
-    >>> msg = six.ensure_str(
+    >>> msg = (
     ...     "Launchpad Administrators has been unsubscribed from this "
     ...     "blueprint.")
     >>> msg in browser.contents
diff --git a/lib/lp/bugs/model/tests/test_bugtasksearch.py b/lib/lp/bugs/model/tests/test_bugtasksearch.py
index a8bd7a7..c19503e 100644
--- a/lib/lp/bugs/model/tests/test_bugtasksearch.py
+++ b/lib/lp/bugs/model/tests/test_bugtasksearch.py
@@ -9,7 +9,6 @@ from operator import attrgetter
 import unittest
 
 import pytz
-import six
 from storm.expr import Or
 from testtools.matchers import Equals
 from testtools.testcase import ExpectedException
@@ -2460,8 +2459,7 @@ def test_suite():
     loader = unittest.TestLoader()
     for bug_target_search_type_class in (
         PreloadBugtaskTargets, NoPreloadBugtaskTargets, QueryBugIDs):
-        class_name = six.ensure_str(
-            'Test%s' % bug_target_search_type_class.__name__)
+        class_name = 'Test%s' % bug_target_search_type_class.__name__
         class_bases = (
             bug_target_search_type_class, ProductTarget, OnceTests,
             SearchTestBase, TestCaseWithFactory)
@@ -2469,10 +2467,9 @@ def test_suite():
         suite.addTest(loader.loadTestsFromTestCase(test_class))
 
         for target_mixin in bug_targets_mixins:
-            class_name = six.ensure_str(
-                'Test%s%s' % (
-                    bug_target_search_type_class.__name__,
-                    target_mixin.__name__))
+            class_name = 'Test%s%s' % (
+                bug_target_search_type_class.__name__,
+                target_mixin.__name__)
             mixins = [
                 target_mixin, bug_target_search_type_class]
             class_bases = (
diff --git a/lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt b/lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt
index d58193d..7a7ebf8 100644
--- a/lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt
+++ b/lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt
@@ -704,7 +704,7 @@ confirmation.
       mailto:dark-master-o-bugs@xxxxxxxxxxxxxxxx
 
     >>> user_browser.contents.count(
-    ...     six.ensure_str('mailto:dark-master-o-bugs@xxxxxxxxxxxxxxxx'))
+    ...     'mailto:dark-master-o-bugs@xxxxxxxxxxxxxxxx')
     3
 
 To evade harvesting, the email address above is obfuscated if you're not
@@ -720,5 +720,5 @@ logged in.
       auto-dark-master-o-bugs
 
     >>> anon_browser.contents.count(
-    ...     six.ensure_str('mailto:dark-master-o-bugs@xxxxxxxxxxxxxxxx'))
+    ...     'mailto:dark-master-o-bugs@xxxxxxxxxxxxxxxx')
     0
diff --git a/lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-page.txt b/lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-page.txt
index 918cf50..e4b2e8d 100644
--- a/lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-page.txt
+++ b/lib/lp/bugs/stories/bug-tags/xx-tags-on-bug-page.txt
@@ -34,11 +34,11 @@ Let's specify two valid tags.
 
 Now the tags will be displayed on the bug page:
 
-    >>> six.ensure_str('Tags:') in user_browser.contents
+    >>> 'Tags:' in user_browser.contents
     True
-    >>> six.ensure_str('foo') in user_browser.contents
+    >>> 'foo' in user_browser.contents
     True
-    >>> six.ensure_str('bar') in user_browser.contents
+    >>> 'bar' in user_browser.contents
     True
 
 Simply changing the ordering of the bug tags won't cause anything to
diff --git a/lib/lp/bugs/stories/bugattachments/xx-bugattachments.txt b/lib/lp/bugs/stories/bugattachments/xx-bugattachments.txt
index ec8e157..ae6db94 100644
--- a/lib/lp/bugs/stories/bugattachments/xx-bugattachments.txt
+++ b/lib/lp/bugs/stories/bugattachments/xx-bugattachments.txt
@@ -44,14 +44,14 @@ We can check that the attachment is there
     >>> link.url
     'http://bugs.launchpad.test/firefox/+bug/1/+attachment/.../+files/foo.txt'
 
-    >>> six.ensure_str('Added some information') in user_browser.contents
+    >>> 'Added some information' in user_browser.contents
     True
 
 And that we stripped the leading and trailing whitespace correctly
 
-    >>> six.ensure_str('   Some information   ') in user_browser.contents
+    >>> '   Some information   ' in user_browser.contents
     False
-    >>> six.ensure_str('Some information') in user_browser.contents
+    >>> 'Some information' in user_browser.contents
     True
 
 If no description is given it gets set to the attachment filename. It's
@@ -265,7 +265,7 @@ We can also edit the attachment details, let's navigate to that page.
     >>> user_browser.url
     'http://bugs.launchpad.test/firefox/+bug/1/+attachment/...'
 
-    >>> six.ensure_str('Edit attachment') in user_browser.contents
+    >>> 'Edit attachment' in user_browser.contents
     True
 
 There's also an option to cancel, which takes you back to the bug
@@ -286,7 +286,7 @@ whitespace to test that's correctly stripped)...
     >>> user_browser.url
     'http://bugs.launchpad.test/firefox/+bug/1'
 
-    >>> six.ensure_str('Another title') in user_browser.contents
+    >>> 'Another title' in user_browser.contents
     True
 
 We can edit the attachment to be a patch.
diff --git a/lib/lp/bugs/stories/bugs/xx-bug-edit.txt b/lib/lp/bugs/stories/bugs/xx-bug-edit.txt
index 7700e4a..2456c33 100644
--- a/lib/lp/bugs/stories/bugs/xx-bug-edit.txt
+++ b/lib/lp/bugs/stories/bugs/xx-bug-edit.txt
@@ -73,9 +73,9 @@ Now we are back at the bug page, and the tag has been added.
 
     >>> user_browser.url
     'http://bugs.launchpad.test/firefox/+bug/1'
-    >>> six.ensure_str('layout-test') in user_browser.contents
+    >>> 'layout-test' in user_browser.contents
     True
-    >>> six.ensure_str('new-tag') in user_browser.contents
+    >>> 'new-tag' in user_browser.contents
     False
 
 Now, let's add 'new-tag' again, and confirm it this time.
diff --git a/lib/lp/bugs/stories/bugs/xx-bug-index.txt b/lib/lp/bugs/stories/bugs/xx-bug-index.txt
index 590df67..540000e 100644
--- a/lib/lp/bugs/stories/bugs/xx-bug-index.txt
+++ b/lib/lp/bugs/stories/bugs/xx-bug-index.txt
@@ -43,7 +43,7 @@ highlighted.
     ...<tr>
     ...mozilla-firefox (Ubuntu)...
     ...
-    >>> anon_browser.contents.count(six.ensure_str('<tr class="highlight"'))
+    >>> anon_browser.contents.count('<tr class="highlight"')
     1
 
     >>> anon_browser.open(
@@ -60,7 +60,7 @@ highlighted.
     ...<tr>
     ...mozilla-firefox (Ubuntu)...
     ...
-    >>> anon_browser.contents.count(six.ensure_str('<tr class="highlight"'))
+    >>> anon_browser.contents.count('<tr class="highlight"')
     1
 
 If the context is a distribution package, the package name has a
diff --git a/lib/lp/bugs/stories/bugs/xx-bug-obfuscation.txt b/lib/lp/bugs/stories/bugs/xx-bug-obfuscation.txt
index a7dc948..7864052 100644
--- a/lib/lp/bugs/stories/bugs/xx-bug-obfuscation.txt
+++ b/lib/lp/bugs/stories/bugs/xx-bug-obfuscation.txt
@@ -29,7 +29,7 @@ An anonymous cannot see the email address anywhere in the page.
     >>> print(anon_browser.title)
     Bug #3 ...
 
-    >>> six.ensure_str('user@xxxxxxxxxx') in anon_browser.contents
+    >>> 'user@xxxxxxxxxx' in anon_browser.contents
     False
 
     >>> description = find_tag_by_id(
diff --git a/lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt b/lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt
index f4e5a39..f25404c 100644
--- a/lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt
+++ b/lib/lp/bugs/stories/bugs/xx-bug-text-pages.txt
@@ -127,7 +127,7 @@ are replaced by a single space.
 
     >>> attachments_text = text_bug[text_bug.find('attachments:'):]
     >>> attachment_2 = attachments_text.split('\n')[2]
-    >>> six.ensure_str(attachment_2)
+    >>> attachment_2
     ' http://bugs.launchpad.test/.../file%20with%20space.txt text/plain;
     name="file with space.txt"'
 
diff --git a/lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.txt b/lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.txt
index 2488478..d86a58d 100644
--- a/lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.txt
+++ b/lib/lp/bugs/stories/bugwatches/xx-edit-bugwatch.txt
@@ -10,9 +10,8 @@ After a bug watch is recorded, it is possible to go back and change it.
     >>> admin_browser.getControl('Change').click()
     >>> admin_browser.url
     'http://bugs.launchpad.test/firefox/+bug/1'
-    >>> six.ensure_str(
-    ...     'https://bugzilla.mozilla.org/show_bug.cgi?id=1000') in (
-    ...         admin_browser.contents)
+    >>> 'https://bugzilla.mozilla.org/show_bug.cgi?id=1000' in (
+    ...     admin_browser.contents)
     True
 
 The URL supplied must be a valid bug tracker URL and must point to a
diff --git a/lib/lp/code/browser/tests/test_codereviewcomment.py b/lib/lp/code/browser/tests/test_codereviewcomment.py
index 458f67a..ac402fe 100644
--- a/lib/lp/code/browser/tests/test_codereviewcomment.py
+++ b/lib/lp/code/browser/tests/test_codereviewcomment.py
@@ -207,7 +207,7 @@ class TestCodeReviewCommentHtmlMixin:
         contents = 'test-comment'
         comment = self.makeCodeReviewComment(body=contents)
         browser = self.getViewBrowser(comment, view_name='+reply')
-        self.assertIn(six.ensure_str(contents), browser.contents)
+        self.assertIn(contents, browser.contents)
 
     def test_footer_for_mergeable_and_admin(self):
         """An admin sees Hide/Reply links for a comment on a mergeable MP."""
diff --git a/lib/lp/code/model/branch.py b/lib/lp/code/model/branch.py
index 98952be..f86f754 100644
--- a/lib/lp/code/model/branch.py
+++ b/lib/lp/code/model/branch.py
@@ -743,7 +743,7 @@ class Branch(SQLBase, WebhookTargetMixin, BzrIdentityMixin):
 
     def getInternalBzrUrl(self):
         """See `IBranch`."""
-        return six.ensure_str('lp-internal:///' + self.unique_name)
+        return 'lp-internal:///' + self.unique_name
 
     def getBzrBranch(self):
         """See `IBranch`."""
diff --git a/lib/lp/coop/answersbugs/visibility.py b/lib/lp/coop/answersbugs/visibility.py
index b167ef2..cbb3815 100644
--- a/lib/lp/coop/answersbugs/visibility.py
+++ b/lib/lp/coop/answersbugs/visibility.py
@@ -8,8 +8,6 @@ __all__ = [
     'TestMessageVisibilityMixin',
     ]
 
-import six
-
 from lp.services.webapp.escaping import html_escape
 from lp.testing.pages import find_tag_by_id
 
@@ -17,7 +15,7 @@ from lp.testing.pages import find_tag_by_id
 class TestMessageVisibilityMixin:
 
     comment_text = "You can't see me."
-    html_comment_text = six.ensure_str(html_escape(comment_text))
+    html_comment_text = html_escape(comment_text)
 
     def makeHiddenMessage(self):
         """To be overwridden by subclasses.
diff --git a/lib/lp/registry/browser/tests/person-admin-views.txt b/lib/lp/registry/browser/tests/person-admin-views.txt
index 9e4d33d..34d0d8f 100644
--- a/lib/lp/registry/browser/tests/person-admin-views.txt
+++ b/lib/lp/registry/browser/tests/person-admin-views.txt
@@ -129,7 +129,7 @@ an account is suspended, the preferred email address is disabled.
     >>> transaction.commit()
     >>> user.account_status
     <DBItem AccountStatus.SUSPENDED, ...>
-    >>> six.ensure_str(user.account_status_history)
+    >>> user.account_status_history
     '... name16: Active -> Suspended: Wanted by the galactic police.\n'
     >>> print(user.preferredemail)
     None
@@ -161,7 +161,7 @@ user must log in to restore the email addresses using the reactivate step.
     []
     >>> user.account_status
     <DBItem AccountStatus.DEACTIVATED, ...>
-    >>> six.ensure_str(user.account_status_history)
+    >>> user.account_status_history
     "... name16: Active -> Suspended: Wanted by the galactic police.\n...
     name16: Suspended -> Deactivated: Zaphod's a hoopy frood.\n"
     >>> print(user.preferredemail)
@@ -180,7 +180,7 @@ An admin can mark an account as belonging to a user who has died.
     []
     >>> user.account_status
     <DBItem AccountStatus.DECEASED, ...>
-    >>> six.ensure_str(user.account_status_history)
+    >>> user.account_status_history
     "... name16: Active -> Suspended: Wanted by the galactic police.\n...
     name16: Suspended -> Deactivated: Zaphod's a hoopy frood.\n...
     name16: Deactivated -> Deceased: In memoriam.\n"
diff --git a/lib/lp/registry/browser/tests/test_announcements.py b/lib/lp/registry/browser/tests/test_announcements.py
index 5f005c6..af459b4 100644
--- a/lib/lp/registry/browser/tests/test_announcements.py
+++ b/lib/lp/registry/browser/tests/test_announcements.py
@@ -7,7 +7,6 @@ from datetime import datetime
 
 from lxml import html
 from pytz import utc
-import six
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -107,7 +106,7 @@ class TestAnnouncementPage(BrowserTestCase):
         removeSecurityProxy(inactive_product).active = False
 
         browser = self.getViewBrowser(context, view_name, user=user)
-        contents = six.ensure_str(browser.contents, "utf-8")
+        contents = browser.contents
 
         self.assertIn(first_announcement.title, contents)
         self.assertIn(first_announcement.summary, contents)
diff --git a/lib/lp/registry/doc/person-account.txt b/lib/lp/registry/doc/person-account.txt
index 576ab7d..0ed70c6 100644
--- a/lib/lp/registry/doc/person-account.txt
+++ b/lib/lp/registry/doc/person-account.txt
@@ -49,7 +49,7 @@ Matsubara can.
     True
     >>> matsubara.account.status
     <DBItem AccountStatus.ACTIVE, ...>
-    >>> six.ensure_str(removeSecurityProxy(matsubara.account).status_history)
+    >>> removeSecurityProxy(matsubara.account).status_history
     '...: Unactivated -> Active: test\n'
     >>> print(removeSecurityProxy(matsubara.preferredemail).email)
     matsubara@xxxxxxxxxxxx
@@ -156,7 +156,7 @@ adds a '-deactivatedaccount' suffix to the person's name...
     >>> foobar.account.status
     <DBItem AccountStatus.DEACTIVATED...
 
-    >>> six.ensure_str(removeSecurityProxy(foobar.account).status_history)
+    >>> removeSecurityProxy(foobar.account).status_history
     "... name16: Active -> Deactivated:
     I'm a person who doesn't want to be listed as a Launchpad user.\n"
 
@@ -232,7 +232,7 @@ Accounts can be reactivated.
     >>> foobar.account.status
     <DBItem AccountStatus.ACTIVE...
 
-    >>> six.ensure_str(removeSecurityProxy(foobar.account).status_history)
+    >>> removeSecurityProxy(foobar.account).status_history
     "... name16: Active -> Deactivated: I'm a person
     who doesn't want to be listed as a Launchpad user.\n...:
     Deactivated -> Active:
diff --git a/lib/lp/registry/scripts/tests/test_closeaccount.py b/lib/lp/registry/scripts/tests/test_closeaccount.py
index ada1dd5..66277ea 100644
--- a/lib/lp/registry/scripts/tests/test_closeaccount.py
+++ b/lib/lp/registry/scripts/tests/test_closeaccount.py
@@ -3,7 +3,6 @@
 
 """Test the close-account script."""
 
-import six
 from storm.store import Store
 from testtools.matchers import (
     MatchesSetwise,
@@ -178,7 +177,7 @@ class TestCloseAccount(TestCaseWithFactory):
         person_id = person.id
         account_id = person.account.id
         self.factory.makeProduct(owner=person)
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.assertRaisesWithContent(
                 LaunchpadScriptFailure,
@@ -192,7 +191,7 @@ class TestCloseAccount(TestCaseWithFactory):
 
     def test_dry_run(self):
         person, person_id, account_id = self.makePopulatedUser()
-        script = self.makeScript(['--dry-run', six.ensure_str(person.name)])
+        script = self.makeScript(['--dry-run', person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertIn(
@@ -201,14 +200,14 @@ class TestCloseAccount(TestCaseWithFactory):
 
     def test_single_by_name(self):
         person, person_id, account_id = self.makePopulatedUser()
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
 
     def test_single_by_email(self):
         person, person_id, account_id = self.makePopulatedUser()
-        script = self.makeScript([six.ensure_str(person.preferredemail.email)])
+        script = self.makeScript([person.preferredemail.email])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -227,7 +226,7 @@ class TestCloseAccount(TestCaseWithFactory):
     def test_multiple_email_addresses(self):
         person, person_id, account_id = self.makePopulatedUser()
         self.factory.makeEmail('%s@xxxxxxxxxxxxxxxxxxx' % person.name, person)
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -251,7 +250,7 @@ class TestCloseAccount(TestCaseWithFactory):
         snap = self.factory.makeSnap()
         snap_build = self.factory.makeSnapBuild(requester=person, snap=snap)
         specification = self.factory.makeSpecification(drafter=person)
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -271,7 +270,7 @@ class TestCloseAccount(TestCaseWithFactory):
             question.addComment(person, "comment")
             removeSecurityProxy(question).status = status
             questions.append(question)
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -293,7 +292,7 @@ class TestCloseAccount(TestCaseWithFactory):
             question.addComment(person, "comment")
             removeSecurityProxy(question).status = status
             questions[status] = question
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -308,7 +307,7 @@ class TestCloseAccount(TestCaseWithFactory):
         spec = self.factory.makeSpecification()
         branch = self.factory.makeAnyBranch()
         spec.linkBranch(branch, person)
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -331,7 +330,7 @@ class TestCloseAccount(TestCaseWithFactory):
             while not job.isDone():
                 job(chunk_size=100)
         self.assertTrue(person.hasMaintainedPackages())
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -345,7 +344,7 @@ class TestCloseAccount(TestCaseWithFactory):
         bugtask = self.factory.makeBugTask(bug=bug, owner=person)
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -358,7 +357,7 @@ class TestCloseAccount(TestCaseWithFactory):
         self.factory.makeBugWatch(owner=person)
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -370,7 +369,7 @@ class TestCloseAccount(TestCaseWithFactory):
         self.assertTrue(bug.isUserAffected(person))
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -382,7 +381,7 @@ class TestCloseAccount(TestCaseWithFactory):
         translations_person.translations_relicensing_agreement = True
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -399,7 +398,7 @@ class TestCloseAccount(TestCaseWithFactory):
                 person, pofile))
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -413,7 +412,7 @@ class TestCloseAccount(TestCaseWithFactory):
         pofile.owner = person
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -429,7 +428,7 @@ class TestCloseAccount(TestCaseWithFactory):
         self.assertIsNotNone(ppa.getAuthToken(person))
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             now = get_transaction_timestamp(Store.of(person))
             self.runScript(script)
@@ -557,7 +556,7 @@ class TestCloseAccount(TestCaseWithFactory):
             submission_ids[0], get_submission_by_submission_key(keys[0]))
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -584,7 +583,7 @@ class TestCloseAccount(TestCaseWithFactory):
             MatchesStructure.byEquality(count=1, viewed_by=other_person)))
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -620,7 +619,7 @@ class TestCloseAccount(TestCaseWithFactory):
             MatchesStructure.byEquality(owner=other_person)))
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -634,7 +633,7 @@ class TestCloseAccount(TestCaseWithFactory):
         product.active = False
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -652,7 +651,7 @@ class TestCloseAccount(TestCaseWithFactory):
                 TargetRevisionControlSystems.GIT)]
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -680,7 +679,7 @@ class TestCloseAccount(TestCaseWithFactory):
                 result_status=CodeImportResultStatus.SUCCESS)
             person_id = person.id
             account_id = person.account.id
-            script = self.makeScript([six.ensure_str(person.name)])
+            script = self.makeScript([person.name])
             with dbuser('launchpad'):
                 self.runScript(script)
             self.assertRemoved(account_id, person_id)
@@ -708,7 +707,7 @@ class TestCloseAccount(TestCaseWithFactory):
         removeSecurityProxy(ppa).owner = other_person
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -719,7 +718,7 @@ class TestCloseAccount(TestCaseWithFactory):
         person_id = person.id
         account_id = person.account.id
         specification = self.factory.makeSpecification(owner=person)
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -738,7 +737,7 @@ class TestCloseAccount(TestCaseWithFactory):
 
         person_id = member.id
         account_id = member.account.id
-        script = self.makeScript([six.ensure_str(member.name)])
+        script = self.makeScript([member.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -751,7 +750,7 @@ class TestCloseAccount(TestCaseWithFactory):
         owned_team2 = self.factory.makeTeam(name='target2', owner=person)
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
 
         # Closing account fails as the user still owns team2
         with dbuser('launchpad'):
@@ -776,7 +775,7 @@ class TestCloseAccount(TestCaseWithFactory):
         self.assertEqual(token, login_token_set[plaintext_token])
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -794,7 +793,7 @@ class TestCloseAccount(TestCaseWithFactory):
             [other_request_token], other_person.oauth_request_tokens)
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -813,7 +812,7 @@ class TestCloseAccount(TestCaseWithFactory):
             [other_access_token], other_person.oauth_access_tokens)
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
@@ -828,7 +827,7 @@ class TestCloseAccount(TestCaseWithFactory):
         ppa.setProcessors(procs)
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.assertRaisesWithContent(
                 LaunchpadScriptFailure,
@@ -851,7 +850,7 @@ class TestCloseAccount(TestCaseWithFactory):
             DevNullLogger(), getPubConfig(ppa), None, ppa).deleteArchive()
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.assertRaisesWithContent(
                 LaunchpadScriptFailure,
@@ -873,7 +872,7 @@ class TestCloseAccount(TestCaseWithFactory):
         store = Store.of(ppa)
         person_id = person.id
         account_id = person.account.id
-        script = self.makeScript([six.ensure_str(person.name)])
+        script = self.makeScript([person.name])
         with dbuser('launchpad'):
             self.runScript(script)
         self.assertRemoved(account_id, person_id)
diff --git a/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt b/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
index 1b81c27..d9c465e 100644
--- a/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
+++ b/lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt
@@ -111,7 +111,7 @@ Import the secret keys needed for this test:
 access the current IGpghandler instance to access this key and decrypt the
 message.
 
-    >>> body = decrypt_content(cipher_body, six.ensure_str('test'))
+    >>> body = decrypt_content(cipher_body, 'test')
 
 Extract the token URL from the email:
 
@@ -514,7 +514,7 @@ Get the token from the body of the email sent.
     >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
     >>> msg = email.message_from_bytes(raw_msg)
     >>> cipher_body = msg.get_payload(decode=1)
-    >>> body = decrypt_content(cipher_body, six.ensure_str('test'))
+    >>> body = decrypt_content(cipher_body, 'test')
     >>> link = get_token_url_from_bytes(body)
     >>> token = re.sub(r'.*token/', '', link)
     >>> token_url = 'http://launchpad.test/token/%s' % token
diff --git a/lib/lp/services/database/doc/enumcol.txt b/lib/lp/services/database/doc/enumcol.txt
index 4685182..9cb728b 100644
--- a/lib/lp/services/database/doc/enumcol.txt
+++ b/lib/lp/services/database/doc/enumcol.txt
@@ -85,7 +85,7 @@ You cannot use integers or strings as DBEnum values:
     ...
     TypeError: Not a DBItem: 2
 
-    >>> t.foo = six.ensure_str("TWO")
+    >>> t.foo = "TWO"
     Traceback (most recent call last):
     ...
     TypeError: Not a DBItem: 'TWO'
diff --git a/lib/lp/services/database/doc/textsearching.txt b/lib/lp/services/database/doc/textsearching.txt
index 6aa25a7..5ced23b 100644
--- a/lib/lp/services/database/doc/textsearching.txt
+++ b/lib/lp/services/database/doc/textsearching.txt
@@ -121,7 +121,7 @@ The following examples show the text version of the query using
     ...         "SELECT to_tsvector(%s) @@ ftq(%s)",
     ...         (text_to_search, search_phrase))
     ...     match = result.get_all()[0][0]
-    ...     return six.ensure_str("FTI data: %s query: %s match: %s") % (
+    ...     return "FTI data: %s query: %s match: %s" % (
     ...         ts_vector, ts_query, str(match))
     >>>
     >>> def search_same(text):
@@ -248,7 +248,7 @@ Punctuation is handled consistently. If a string containing punctuation
 appears in an FTI, it can also be passed to ftq(),and a search for this
 string finds the indexed text.
 
-    >>> punctuation = six.ensure_str('\'"#$%*+,./:;<=>?@[\]^`{}~')
+    >>> punctuation = '\'"#$%*+,./:;<=>?@[\]^`{}~'
     >>> for symbol in punctuation:
     ...     print(repr(symbol), search_same('foo%sbar' % symbol))
     "'" FTI data: 'bar':2 'foo':1 query: 'foo' & 'bar' match: True
diff --git a/lib/lp/services/doc/limitedlist.txt b/lib/lp/services/doc/limitedlist.txt
index e8bdf08..10f1fed 100644
--- a/lib/lp/services/doc/limitedlist.txt
+++ b/lib/lp/services/doc/limitedlist.txt
@@ -22,7 +22,7 @@ We can optionally specify the initial content of the sequence. Note that
 only the last N elements of the second parameter are stored, where N is
 the given maximum size of the LimitedList.
 
-    >>> LimitedList(3, (0, six.ensure_str('one'), 2, 3))
+    >>> LimitedList(3, (0, 'one', 2, 3))
     <LimitedList(3, ['one', 2, 3])>
 
 If the initial content has more elements than the given maximum length,
diff --git a/lib/lp/services/gpg/doc/gpg-encryption.txt b/lib/lp/services/gpg/doc/gpg-encryption.txt
index b1a1b0c..21da208 100644
--- a/lib/lp/services/gpg/doc/gpg-encryption.txt
+++ b/lib/lp/services/gpg/doc/gpg-encryption.txt
@@ -57,7 +57,7 @@ cipher constains the encrypted content.
 Storing the raw password may compromise the security, but is the
 only way we can decrypt the cipher content.
 
-    >>> password = six.ensure_str('test')
+    >>> password = 'test'
     >>> plain = decrypt_content(cipher, password)
 
 voilá, the same content shows up again.
@@ -69,7 +69,7 @@ Verify if the encrytion process support passing another charset string
 
     >>> content = u'a\xe7ucar'
     >>> cipher = gpghandler.encryptContent(content.encode('iso-8859-1'), key)
-    >>> plain = decrypt_content(cipher, six.ensure_str('test'))
+    >>> plain = decrypt_content(cipher, 'test')
     >>> print(backslashreplace(plain.decode('iso-8859-1')))
     a\xe7ucar
 
@@ -85,7 +85,7 @@ Decrypt a unicode content:
     >>> content = u'a\xe7ucar'
     >>> cipher = gpghandler.encryptContent(content.encode('iso-8859-1'), key)
     >>> cipher = six.ensure_text(cipher)
-    >>> plain = decrypt_content(cipher, six.ensure_str('test'))
+    >>> plain = decrypt_content(cipher, 'test')
     Traceback (most recent call last):
     ...
     TypeError: Content must be bytes.
@@ -113,7 +113,7 @@ What about a message encrypted for an unknown key.
     ... =LQK5
     ... -----END PGP MESSAGE-----
     ... """
-    >>> plain = decrypt_content(cipher, six.ensure_str('test'))
+    >>> plain = decrypt_content(cipher, 'test')
     >>> plain is None
     True
 
diff --git a/lib/lp/services/gpg/handler.py b/lib/lp/services/gpg/handler.py
index bad85f1..005ca79 100644
--- a/lib/lp/services/gpg/handler.py
+++ b/lib/lp/services/gpg/handler.py
@@ -341,8 +341,7 @@ class GPGHandler:
         # See more information at:
         # http://pyme.sourceforge.net/doc/gpgme/Generating-Keys.html
         with gpgme_timeline("genkey", name):
-            result = context.genkey(
-                six.ensure_str(signing_only_param % {'name': name}))
+            result = context.genkey(signing_only_param % {'name': name})
 
         # Right, it might seem paranoid to have this many assertions,
         # but we have to take key generation very seriously.
diff --git a/lib/lp/services/gpg/tests/test_gpghandler.py b/lib/lp/services/gpg/tests/test_gpghandler.py
index 6dba52c..4072646 100644
--- a/lib/lp/services/gpg/tests/test_gpghandler.py
+++ b/lib/lp/services/gpg/tests/test_gpghandler.py
@@ -77,9 +77,9 @@ class FakeGenerateKey(Fixture):
                 self.secret_key)
 
             # Fail if the key generation parameters aren't what we expect.
-            expected_params = six.ensure_str(signing_only_param % {
+            expected_params = signing_only_param % {
                 "name": imported_key.uids[0].name,
-                })
+                }
             if params != expected_params:
                 raise ValueError(
                     "Got params %r, expected %r" % (params, expected_params))
diff --git a/lib/lp/services/librarian/model.py b/lib/lp/services/librarian/model.py
index 10a387e..6b0ea19 100644
--- a/lib/lp/services/librarian/model.py
+++ b/lib/lp/services/librarian/model.py
@@ -15,7 +15,6 @@ import hashlib
 
 from lazr.delegates import delegate_to
 import pytz
-import six
 from six.moves.urllib.parse import urlparse
 from storm.locals import (
     Date,
@@ -358,7 +357,7 @@ class TimeLimitedToken(StormBase):
         # allocation the external librarian must be able to serve the file
         # immediately.
         store.commit()
-        return six.ensure_str(token)
+        return token
 
     @staticmethod
     def url_to_token_path(url):
diff --git a/lib/lp/services/mail/doc/notification-recipient-set.txt b/lib/lp/services/mail/doc/notification-recipient-set.txt
index d06e7c4..6ed6b2c 100644
--- a/lib/lp/services/mail/doc/notification-recipient-set.txt
+++ b/lib/lp/services/mail/doc/notification-recipient-set.txt
@@ -120,7 +120,7 @@ UnknownRecipientError:
       ...
     lp.services.mail.interfaces.UnknownRecipientError: ...
 
-    >>> recipients.getReason(six.ensure_str('no-priv@xxxxxxxxxxxxx'))
+    >>> recipients.getReason('no-priv@xxxxxxxxxxxxx')
     Traceback (most recent call last):
       ...
     lp.services.mail.interfaces.UnknownRecipientError: 'no-priv@xxxxxxxxxxxxx'
diff --git a/lib/lp/services/webapp/breadcrumb.py b/lib/lp/services/webapp/breadcrumb.py
index 1281fe2..eae2074 100644
--- a/lib/lp/services/webapp/breadcrumb.py
+++ b/lib/lp/services/webapp/breadcrumb.py
@@ -10,7 +10,6 @@ __all__ = [
     'TitleBreadcrumb',
     ]
 
-import six
 from zope.interface import implementer
 
 from lp.services.webapp import canonical_url
@@ -77,12 +76,8 @@ class Breadcrumb:
         return self._detail or self.text
 
     def __repr__(self):
-        # XXX: salgado, 2009-10-14, http://bugs.python.org/issue5876: In
-        # python 2.5-2.7, the return value of __repr__() may be forced into a
-        # type(str), so we can't include unicode here.
-        text = six.ensure_str(self.text, 'raw-unicode-escape')
         return "<%s url='%s' text='%s'>" % (
-            self.__class__.__name__, self.url, text)
+            self.__class__.__name__, self.url, self.text)
 
 
 class NameBreadcrumb(Breadcrumb):
diff --git a/lib/lp/services/webapp/doc/canonical_url.txt b/lib/lp/services/webapp/doc/canonical_url.txt
index 90d4df5..60f824a 100644
--- a/lib/lp/services/webapp/doc/canonical_url.txt
+++ b/lib/lp/services/webapp/doc/canonical_url.txt
@@ -18,8 +18,7 @@ will put in a temporary module.
     >>> from lp.services.webapp.interfaces import ICanonicalUrlData
     >>> from zope.interface import Interface, Attribute, implementer
 
-    >>> module = types.ModuleType(
-    ...     six.ensure_str(factory.getUniqueString().replace('-', '_')))
+    >>> module = types.ModuleType(factory.getUniqueString().replace('-', '_'))
     >>> sys.modules[module.__name__] = module
 
     >>> class ICountrySet(Interface):
diff --git a/lib/lp/services/webapp/doc/menus.txt b/lib/lp/services/webapp/doc/menus.txt
index c1cf9b4..7f910c8 100644
--- a/lib/lp/services/webapp/doc/menus.txt
+++ b/lib/lp/services/webapp/doc/menus.txt
@@ -561,8 +561,7 @@ First, we define a couple of interfaces, and put them in a temporary module.
     >>> import sys
     >>> import types
 
-    >>> module = types.ModuleType(
-    ...     six.ensure_str(factory.getUniqueString().replace('-', '_')))
+    >>> module = types.ModuleType(factory.getUniqueString().replace('-', '_'))
     >>> sys.modules[module.__name__] = module
 
     >>> class IThingHavingFacets(Interface):
diff --git a/lib/lp/services/webapp/doc/test_adapter.txt b/lib/lp/services/webapp/doc/test_adapter.txt
index abe5036..04378eb 100644
--- a/lib/lp/services/webapp/doc/test_adapter.txt
+++ b/lib/lp/services/webapp/doc/test_adapter.txt
@@ -211,7 +211,7 @@ timeout by sleeping for 200ms with a 100ms statement timeout:
     >>> set_request_started()
     >>> print(current_statement_timeout(store))
     100ms
-    >>> store.execute(six.ensure_str('SELECT pg_sleep(0.200)'), noresult=True)
+    >>> store.execute('SELECT pg_sleep(0.200)', noresult=True)
     Traceback (most recent call last):
       ...
     lp.services.webapp.adapter.LaunchpadTimeoutError:
@@ -285,7 +285,7 @@ issuing the SQL command as we have exceeded the precision period:
 This final invokation, we will actually sleep to ensure that the
 timeout being reported by PostgreSQL is actually working:
 
-    >>> store.execute(six.ensure_str('SELECT pg_sleep(0.2)'), noresult=True)
+    >>> store.execute('SELECT pg_sleep(0.2)', noresult=True)
     Traceback (most recent call last):
       ...
     lp.services.webapp.adapter.LaunchpadTimeoutError:
diff --git a/lib/lp/services/webapp/publication.py b/lib/lp/services/webapp/publication.py
index 80effc1..37cd383 100644
--- a/lib/lp/services/webapp/publication.py
+++ b/lib/lp/services/webapp/publication.py
@@ -17,7 +17,6 @@ from lazr.uri import (
     URI,
     )
 from psycopg2.extensions import TransactionRollbackError
-import six
 from six.moves.urllib.parse import quote
 from storm.database import STATE_DISCONNECTED
 from storm.exceptions import (
@@ -422,10 +421,7 @@ class LaunchpadBrowserPublication(
                 return self.constructPageID(context, context.context, names)
             view_names = ':'.join(names)
             pageid = '%s:%s' % (context_name, view_names)
-        # The view name used in the pageid usually comes from ZCML and so it
-        # will be a Unicode string, but we want a native string.  On Python
-        # 2, to avoid problems we encode it into ASCII.
-        return six.ensure_str(pageid, 'US-ASCII')
+        return pageid
 
     def callObject(self, request, ob):
         """See `zope.publisher.interfaces.IPublication`.
diff --git a/lib/lp/services/webapp/publisher.py b/lib/lp/services/webapp/publisher.py
index c0f00aa..2f5960d 100644
--- a/lib/lp/services/webapp/publisher.py
+++ b/lib/lp/services/webapp/publisher.py
@@ -504,8 +504,7 @@ class LaunchpadView(UserAttributeCache):
         """See IBrowserPublisher."""
         # By default, a LaunchpadView cannot be traversed through.
         # Those that can be must override this method.
-        raise NotFound(
-            self, six.ensure_str(name, errors='replace'), request=request)
+        raise NotFound(self, name, request=request)
 
     @property
     def recommended_canonical_url(self):
@@ -964,8 +963,7 @@ class Navigation:
         """
         # Avoid circular imports.
         if nextobj is None:
-            raise NotFound(
-                self.context, six.ensure_str(name, errors='replace'))
+            raise NotFound(self.context, name)
         elif isinstance(nextobj, redirection):
             return RedirectionView(
                 nextobj.name, request, status=nextobj.status)
@@ -1185,7 +1183,7 @@ class RenamedView:
 
     def publishTraverse(self, request, name):
         """See zope.publisher.interfaces.browser.IBrowserPublisher."""
-        raise NotFound(self.context, six.ensure_str(name, errors='replace'))
+        raise NotFound(self.context, name)
 
     def browserDefault(self, request):
         """See zope.publisher.interfaces.browser.IBrowserPublisher."""
diff --git a/lib/lp/services/webapp/servers.py b/lib/lp/services/webapp/servers.py
index 32ebf7f..e644e05 100644
--- a/lib/lp/services/webapp/servers.py
+++ b/lib/lp/services/webapp/servers.py
@@ -870,9 +870,7 @@ class LaunchpadBrowserResponse(NotificationResponse, BrowserResponse):
                 status = 307
             else:
                 status = 303
-        super().redirect(
-            six.ensure_str(str(location)),
-            status=status, trusted=trusted)
+        super().redirect(str(location), status=status, trusted=trusted)
 
 
 def adaptResponseToSession(response):
diff --git a/lib/lp/services/webservice/doc/launchpadlib.txt b/lib/lp/services/webservice/doc/launchpadlib.txt
index 25d2e0b..20288ba 100644
--- a/lib/lp/services/webservice/doc/launchpadlib.txt
+++ b/lib/lp/services/webservice/doc/launchpadlib.txt
@@ -5,9 +5,7 @@ Just to show that we're actually talking to the appserver, first check to see
 if a specific user exists...
 
     >>> browser = Browser()
-    >>> browser.addHeader(
-    ...     'Authorization',
-    ...     six.ensure_str('Basic foo.bar@xxxxxxxxxxxxx:test'))
+    >>> browser.addHeader('Authorization', 'Basic foo.bar@xxxxxxxxxxxxx:test')
     >>> from lp.testing.layers import BaseLayer
     >>> root_url = BaseLayer.appserver_root_url()
     >>> browser.open(root_url)
diff --git a/lib/lp/soyuz/scripts/gina/packages.py b/lib/lp/soyuz/scripts/gina/packages.py
index e8b6dae..15b4a45 100644
--- a/lib/lp/soyuz/scripts/gina/packages.py
+++ b/lib/lp/soyuz/scripts/gina/packages.py
@@ -110,9 +110,7 @@ def unpack_dsc(package, version, component, distro_name, archive_root):
         extract_dpkg_source(dsc_path, ".", vendor=distro_name)
     except DpkgSourceError as e:
         if os.path.isdir(source_dir):
-            # Coerce to str to avoid https://bugs.python.org/issue24672 on
-            # Python 2.
-            shutil.rmtree(six.ensure_str(source_dir))
+            shutil.rmtree(source_dir)
         raise ExecutionError("Error %d unpacking source" % e.result)
 
     return source_dir, dsc_path
@@ -149,9 +147,7 @@ def read_dsc(package, version, component, distro_name, archive_root):
                 "No copyright file found for %s in %s" % (package, source_dir))
             copyright = b''
     finally:
-        # Coerce to str to avoid https://bugs.python.org/issue24672 on
-        # Python 2.
-        shutil.rmtree(six.ensure_str(source_dir))
+        shutil.rmtree(source_dir)
 
     return dsc, changelog, copyright
 
diff --git a/lib/lp/soyuz/scripts/ppareport.py b/lib/lp/soyuz/scripts/ppareport.py
index c9e106c..e93c85a 100644
--- a/lib/lp/soyuz/scripts/ppareport.py
+++ b/lib/lp/soyuz/scripts/ppareport.py
@@ -15,7 +15,6 @@ import operator
 import os
 import sys
 
-import six
 from storm.locals import Join
 from storm.store import Store
 from zope.component import getUtility
@@ -175,7 +174,7 @@ class PPAReportScript(LaunchpadScript):
             if size <= (threshold * limit):
                 continue
             line = "%s | %d | %d\n" % (canonical_url(ppa), limit, size)
-            self.output.write(six.ensure_str(line))
+            self.output.write(line)
         self.output.write('\n')
 
     def reportUserEmails(self):
@@ -188,7 +187,7 @@ class PPAReportScript(LaunchpadScript):
         for user in sorted_people_to_email:
             line = "%s | %s | %s\n" % (
                 user.name, user.displayname, user.preferredemail.email)
-            self.output.write(six.ensure_str(line))
+            self.output.write(line)
         self.output.write('\n')
 
     @cachedproperty
diff --git a/lib/lp/soyuz/stories/soyuz/xx-builds-pages.txt b/lib/lp/soyuz/stories/soyuz/xx-builds-pages.txt
index 6b1ab8c..4c49953 100644
--- a/lib/lp/soyuz/stories/soyuz/xx-builds-pages.txt
+++ b/lib/lp/soyuz/stories/soyuz/xx-builds-pages.txt
@@ -21,8 +21,7 @@ according to the selected buildstate, by default All.
     ...     provide only part of the required arguments to enable filter by
     ...     name.
     ...     """
-    ...     assert six.ensure_str(
-    ...         '<input type="submit" value="Filter" />') in contents
+    ...     assert '<input type="submit" value="Filter" />' in contents
     ...
     ...     field_state = find_tag_by_id(contents, 'build_state') is not None
     ...     field_name = find_tag_by_id(contents, 'build_text') is not None
diff --git a/lib/lp/translations/model/pomsgid.py b/lib/lp/translations/model/pomsgid.py
index 85bcd87..5f38c08 100644
--- a/lib/lp/translations/model/pomsgid.py
+++ b/lib/lp/translations/model/pomsgid.py
@@ -50,5 +50,5 @@ class POMsgID(StormBase):
             Func('sha1', POMsgID.msgid) ==
                 Func('sha1', six.ensure_text(key))).one()
         if r is None:
-            raise NotFoundError(six.ensure_str(key, errors='replace'))
+            raise NotFoundError(key)
         return r
diff --git a/lib/lp/translations/model/potranslation.py b/lib/lp/translations/model/potranslation.py
index b342098..9c01c45 100644
--- a/lib/lp/translations/model/potranslation.py
+++ b/lib/lp/translations/model/potranslation.py
@@ -51,7 +51,7 @@ class POTranslation(StormBase):
         if r is not None:
             return r
         else:
-            raise NotFoundError(six.ensure_str(key, errors='replace'))
+            raise NotFoundError(key)
 
     @classmethod
     def getOrCreateTranslation(cls, key):
diff --git a/lib/lp/translations/stories/standalone/xx-language.txt b/lib/lp/translations/stories/standalone/xx-language.txt
index 438f687..0476573 100644
--- a/lib/lp/translations/stories/standalone/xx-language.txt
+++ b/lib/lp/translations/stories/standalone/xx-language.txt
@@ -274,9 +274,8 @@ Changing values to correct content works:
     >>> admin_browser.getControl('ISO 639').value = 'bars'
     >>> admin_browser.getControl('English name').value = 'Changed field'
     >>> spokenin_control = admin_browser.getControl(name='field.countries')
-    >>> spokenin_control.getControl(
-    ...     six.ensure_str('Argentina')).selected = False
-    >>> spokenin_control.getControl(six.ensure_str('France')).selected = True
+    >>> spokenin_control.getControl('Argentina').selected = False
+    >>> spokenin_control.getControl('France').selected = True
     >>> admin_browser.getControl('Admin Language').click()
     >>> print(admin_browser.url)
     http://translations.launchpad.test/+languages/bars
diff --git a/lib/lp/translations/stories/standalone/xx-translationmessage-translate.txt b/lib/lp/translations/stories/standalone/xx-translationmessage-translate.txt
index c049b94..804cd1c 100644
--- a/lib/lp/translations/stories/standalone/xx-translationmessage-translate.txt
+++ b/lib/lp/translations/stories/standalone/xx-translationmessage-translate.txt
@@ -331,7 +331,7 @@ Now, we will check suggestions in this form.
 
 Check that suggestions come in from other contexts:
 
-    >>> six.ensure_str("Suggested in") in browser.contents
+    >>> "Suggested in" in browser.contents
     True
 
     >>> find_tag_by_id(browser.contents, 'msgset_143_es_suggestion_697_0')
@@ -340,16 +340,15 @@ Check that suggestions come in from other contexts:
 Check that no other suggestions are presented (since no others are
 relevant for this message):
 
-    >>> six.ensure_str("Suggested by") in browser.contents
+    >>> "Suggested by" in browser.contents
     False
 
-    >>> six.ensure_str("Used in") in browser.contents
+    >>> "Used in" in browser.contents
     False
 
 Check for the translator note:
 
-    >>> note = six.ensure_str(
-    ...     "This is an example of commenttext for a multiline")
+    >>> note = "This is an example of commenttext for a multiline"
     >>> note in browser.contents
     True
 
@@ -410,7 +409,7 @@ And submit it.
 Now, we check that the translation we are going to add is not yet in the
 form, so we can check later that it's added as a suggestion:
 
-    >>> six.ensure_str('foo!!') in fast_submission.contents
+    >>> 'foo!!' in fast_submission.contents
     False
 
 Now, we update the translation in slow_submission.
diff --git a/lib/lp/translations/stories/translations/xx-translations.txt b/lib/lp/translations/stories/translations/xx-translations.txt
index 9c26cb1..ee8309a 100644
--- a/lib/lp/translations/stories/translations/xx-translations.txt
+++ b/lib/lp/translations/stories/translations/xx-translations.txt
@@ -94,7 +94,7 @@ page, and that it has all the data we are expecting, in terms of languages.
     >>> from lp.testing.pages import extract_url_parameter
     >>> browser.open('http://translations.launchpad.test/ubuntu/hoary/'
     ...     '+translations')
-    >>> six.ensure_str('Translation status by language') in browser.contents
+    >>> 'Translation status by language' in browser.contents
     True
     >>> print(browser.getLink('Catalan').url)
     http://translations.launchpad.test/ubuntu/hoary/+lang/ca
@@ -113,7 +113,7 @@ put Afrihili into the list of "preferred languages".
     >>> browser.addHeader('Accept-Language', 'en-us,en;q=0.7,afh;q=0.3')
     >>> browser.open('http://translations.launchpad.test/ubuntu/hoary/'
     ...     '+translations')
-    >>> six.ensure_str('Translation status by language') in browser.contents
+    >>> 'Translation status by language' in browser.contents
     True
     >>> print(browser.getLink('Catalan').url)
     http://translations.launchpad.test/ubuntu/hoary/+lang/ca
@@ -130,9 +130,9 @@ pofile) for evolution-2.2
     >>> browser.open(
     ...     'http://translations.launchpad.test/ubuntu/hoary/+lang/hr'
     ...     '?batch=2')
-    >>> six.ensure_str('Croatian') in browser.contents
+    >>> 'Croatian' in browser.contents
     True
-    >>> six.ensure_str('Translatable templates') in browser.contents
+    >>> 'Translatable templates' in browser.contents
     True
     >>> print(browser.getLink('evolution-2.2').url)  # noqa
     http://translations.launchpad.test/ubuntu/hoary/+source/evolution/+pots/evolution-2.2/hr/+translate
@@ -173,7 +173,7 @@ And finally, we will get pmount.
 
 With its latest translator.
 
-    >>> six.ensure_str('Edgar Bursic') in browser.contents
+    >>> 'Edgar Bursic' in browser.contents
     True
 
 Last translator
@@ -213,7 +213,7 @@ file):
     batch=10
     >>> print(extract_url_parameter(browser.url, 'show'))
     show=untranslated
-    >>> six.ensure_str('10.') in browser.contents
+    >>> '10.' in browser.contents
     True
 
 If everything works out ok, that means that DummyPOFile has actually
@@ -225,9 +225,7 @@ Finally, lets also check that translated entries show up as well.
     >>> browser.getControl('Change').click()
     >>> print(extract_url_parameter(browser.url, 'show'))
     show=translated
-    >>> six.ensure_str(
-    ...     "There are no messages that match this filtering.") in (
-    ...         browser.contents)
+    >>> "There are no messages that match this filtering." in browser.contents
     True
 
 Links to filtered pages
@@ -386,20 +384,20 @@ should see Catalan in the list.
     >>> browser.open(
     ...     'http://translations.launchpad.test/ubuntu/hoary/+source/'
     ...     'evolution/+translations')
-    >>> six.ensure_str('Catalan') in browser.contents
+    >>> 'Catalan' in browser.contents
     True
 
 But also, he doesn't want to see other languages in the list.  So, he
 shouldn't see eg. Japanese.
 
-    >>> six.ensure_str('Japanese') in browser.contents
+    >>> 'Japanese' in browser.contents
     False
 
 Next, if he chooses to view all the languages, he should see Japanese
 among the languages on the page.
 
     >>> browser.getLink('View template & all languages...').click()
-    >>> six.ensure_str('Japanese') in browser.contents
+    >>> 'Japanese' in browser.contents
     True
 
 So, everything is fine, and Carlos can sleep calmly.
diff --git a/scripts/list-team-members b/scripts/list-team-members
index 507d590..b7a7e61 100755
--- a/scripts/list-team-members
+++ b/scripts/list-team-members
@@ -8,8 +8,6 @@ import _pythonpath  # noqa: F401
 import logging
 import sys
 
-import six
-
 from lp.registry.scripts.listteammembers import (
     NoSuchTeamError,
     process_team,
@@ -55,7 +53,7 @@ class ListTeamMembersScript(LaunchpadScript):
             except NoSuchTeamError:
                 raise LaunchpadScriptFailure("No such team: %s" % teamname)
         for detail in sorted(member_details):
-            print(six.ensure_str(detail))
+            print(detail)
 
 
 if __name__ == '__main__':