Index: django/template/__init__.py
===================================================================
--- django/template/__init__.py	(revision 4391)
+++ django/template/__init__.py	(working copy)
@@ -574,6 +574,8 @@
     def args_check(name, func, provided):
         provided = list(provided)
         plen = len(provided)
+        # Check to see if a decorator is providing the real function.
+        func = getattr(func, '_decorated_function', func)
         args, varargs, varkw, defaults = getargspec(func)
         # First argument is filter input.
         args.pop(0)
Index: django/template/defaultfilters.py
===================================================================
--- django/template/defaultfilters.py	(revision 4391)
+++ django/template/defaultfilters.py	(working copy)
@@ -8,24 +8,63 @@
 
 register = Library()
 
+#######################
+# STRING DECORATOR    #
+#######################
+
+def smart_string(obj):
+    # FUTURE: Unicode strings should probably be normalized to a specific
+    # encoding and non-unicode strings should be converted to unicode too.
+#    if isinstance(obj, unicode):
+#        obj = obj.encode(settings.DEFAULT_CHARSET)
+#    else:
+#        obj = unicode(obj, settings.DEFAULT_CHARSET)
+    # FUTURE: Replace dumb string logic below with cool unicode logic above.
+    if not isinstance(obj, basestring):
+        obj = str(obj)
+    return obj
+
+def to_str(func):
+    """
+    Decorator for filters which should only receive strings. The object passed
+    as the first positional argument will be converted to a string.
+    """
+    def _dec(*args, **kwargs):
+        if args:
+            args = list(args)
+            args[0] = smart_string(args[0])
+        return func(*args, **kwargs)
+    # Make sure the internal name is the original function name because this
+    # is the internal name of the filter if passed directly to Library().filter
+    _dec.__name__ = func.__name__
+    # Include a reference to the real function (used to check original
+    # arguments by the template parser).
+    _dec._decorated_function = getattr(func, '_decorated_function', func)
+    return _dec
+
 ###################
 # STRINGS         #
 ###################
 
 
+#@to_str
 def addslashes(value):
     "Adds slashes - useful for passing strings to JavaScript, for example."
     return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'")
+addslashes = to_str(addslashes)
 
+#@to_str
 def capfirst(value):
     "Capitalizes the first character of the value"
-    value = str(value)
     return value and value[0].upper() + value[1:]
-
+capfirst = to_str(capfirst)
+ 
+#@to_str
 def fix_ampersands(value):
     "Replaces ampersands with ``&amp;`` entities"
     from django.utils.html import fix_ampersands
     return fix_ampersands(value)
+fix_ampersands = to_str(fix_ampersands)
 
 def floatformat(text, arg=-1):
     """
@@ -52,7 +91,7 @@
     try:
         d = int(arg)
     except ValueError:
-        return str(f)
+        return smart_string(f)
     m = f - int(f)
     if not m and d < 0:
         return '%d' % int(f)
@@ -60,6 +99,7 @@
         formatstr = '%%.%df' % abs(d)
         return formatstr % f
 
+#@to_str
 def linenumbers(value):
     "Displays text with line numbers"
     from django.utils.html import escape
@@ -69,22 +109,29 @@
     for i, line in enumerate(lines):
         lines[i] = ("%0" + width  + "d. %s") % (i + 1, escape(line))
     return '\n'.join(lines)
+linenumbers = to_str(linenumbers)
 
+#@to_str
 def lower(value):
     "Converts a string into all lowercase"
     return value.lower()
+lower = to_str(lower)
 
+#@to_str
 def make_list(value):
     """
     Returns the value turned into a list. For an integer, it's a list of
     digits. For a string, it's a list of characters.
     """
-    return list(str(value))
+    return list(value)
+make_list = to_str(make_list)
 
+#@to_str
 def slugify(value):
     "Converts to lowercase, removes non-alpha chars and converts spaces to hyphens"
     value = re.sub('[^\w\s-]', '', value).strip().lower()
     return re.sub('[-\s]+', '-', value)
+slugify = to_str(slugify)
 
 def stringformat(value, arg):
     """
@@ -96,14 +143,17 @@
     of Python string formatting
     """
     try:
-        return ("%" + arg) % value
+        return ("%" + str(arg)) % value
     except (ValueError, TypeError):
         return ""
 
+#@to_str
 def title(value):
     "Converts a string into titlecase"
     return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title())
+title = to_str(title)
 
+#@to_str
 def truncatewords(value, arg):
     """
     Truncates a string after a certain number of words
@@ -115,24 +165,30 @@
         length = int(arg)
     except ValueError: # invalid literal for int()
         return value # Fail silently.
-    if not isinstance(value, basestring):
-        value = str(value)
     return truncate_words(value, length)
+truncatewords = to_str(truncatewords)
 
+#@to_str
 def upper(value):
     "Converts a string into all uppercase"
     return value.upper()
+upper = to_str(upper)
 
+#@to_str
 def urlencode(value):
     "Escapes a value for use in a URL"
     import urllib
     return urllib.quote(value)
+urlencode = to_str(urlencode)
 
+#@to_str
 def urlize(value):
     "Converts URLs in plain text into clickable links"
     from django.utils.html import urlize
     return urlize(value, nofollow=True)
+urlize = to_str(urlize)
 
+#@to_str
 def urlizetrunc(value, limit):
     """
     Converts URLs into clickable links, truncating URLs to the given character limit,
@@ -142,11 +198,15 @@
     """
     from django.utils.html import urlize
     return urlize(value, trim_url_limit=int(limit), nofollow=True)
+urlizetrunc = to_str(urlizetrunc)
 
+#@to_str
 def wordcount(value):
     "Returns the number of words"
     return len(value.split())
+wordcount = to_str(wordcount)
 
+#@to_str
 def wordwrap(value, arg):
     """
     Wraps words at specified line length
@@ -154,50 +214,66 @@
     Argument: number of characters to wrap the text at.
     """
     from django.utils.text import wrap
-    return wrap(str(value), int(arg))
+    return wrap(value, int(arg))
+wordwrap = to_str(wordwrap)
 
+#@to_str
 def ljust(value, arg):
     """
     Left-aligns the value in a field of a given width
 
     Argument: field size
     """
-    return str(value).ljust(int(arg))
+    return value.ljust(int(arg))
+ljust = to_str(ljust)
 
+#@to_str
 def rjust(value, arg):
     """
     Right-aligns the value in a field of a given width
 
     Argument: field size
     """
-    return str(value).rjust(int(arg))
+    return value.rjust(int(arg))
+rjust = to_str(rjust)
 
+#@to_str
 def center(value, arg):
     "Centers the value in a field of a given width"
-    return str(value).center(int(arg))
+    return value.center(int(arg))
+center = to_str(center)
 
+#@to_str
 def cut(value, arg):
     "Removes all values of arg from the given string"
     return value.replace(arg, '')
+cut = to_str(cut)
 
 ###################
 # HTML STRINGS    #
 ###################
 
+#@to_str
 def escape(value):
     "Escapes a string's HTML"
     from django.utils.html import escape
     return escape(value)
+escape = to_str(escape)
 
+#@to_str
 def linebreaks(value):
     "Converts newlines into <p> and <br />s"
     from django.utils.html import linebreaks
     return linebreaks(value)
+linebreaks = to_str(linebreaks)
 
+#@to_str
 def linebreaksbr(value):
     "Converts newlines into <br />s"
     return value.replace('\n', '<br />')
+linebreaksbr = to_str(linebreaksbr)
 
+#@to_str
 def removetags(value, tags):
     "Removes a space separated list of [X]HTML tags from the output"
     tags = [re.escape(tag) for tag in tags.split()]
@@ -207,13 +283,14 @@
     value = starttag_re.sub('', value)
     value = endtag_re.sub('', value)
     return value
+removetags = to_str(removetags)
 
+#@to_str
 def striptags(value):
     "Strips all [X]HTML tags"
     from django.utils.html import strip_tags
-    if not isinstance(value, basestring):
-        value = str(value)
     return strip_tags(value)
+striptags = to_str(striptags)
 
 ###################
 # LISTS           #
@@ -248,7 +325,7 @@
 def join(value, arg):
     "Joins a list with a string, like Python's ``str.join(list)``"
     try:
-        return arg.join(map(str, value))
+        return arg.join(map(smart_string, value))
     except AttributeError: # fail silently but nicely
         return value
 
Index: docs/templates_python.txt
===================================================================
--- docs/templates_python.txt	(revision 4391)
+++ docs/templates_python.txt	(working copy)
@@ -654,6 +654,16 @@
 If you leave off the ``name`` argument, as in the second example above, Django
 will use the function's name as the filter name.
 
+Template filters which expect strings
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+If you are writing a template filter which only expects a string as the first
+argument, you should use the included decorator ``to_str`` which will convert
+an object to it's string value before being passed to your function::
+
+    def lower(value):
+    	return value.lower()
+    lower = template.to_str(lower)
+
 Writing custom template tags
 ----------------------------
 
Index: tests/regressiontests/defaultfilters/tests.py
===================================================================
--- tests/regressiontests/defaultfilters/tests.py	(revision 4391)
+++ tests/regressiontests/defaultfilters/tests.py	(working copy)
@@ -372,8 +372,54 @@
 >>> phone2numeric('0800 flowers')
 '0800 3569377'
 
+# Filters shouldn't break if passed non-strings
+>>> addslashes(123)
+'123'
+>>> linenumbers(123)
+'1. 123'
+>>> lower(123)
+'123'
+>>> make_list(123)
+['1', '2', '3']
+>>> slugify(123)
+'123'
+>>> title(123)
+'123'
+>>> truncatewords(123, 2)
+'123'
+>>> upper(123)
+'123'
+>>> urlencode(123)
+'123'
+>>> urlize(123)
+'123'
+>>> urlizetrunc(123, 1)
+'123'
+>>> wordcount(123)
+1
+>>> wordwrap(123, 2)
+'123'
+>>> ljust('123', 4)
+'123 '
+>>> rjust('123', 4)
+' 123'
+>>> center('123', 5)
+' 123 '
+>>> center('123', 6)
+' 123  '
+>>> cut(123, '2')
+'13'
+>>> escape(123)
+'123'
+>>> linebreaks(123)
+'<p>123</p>'
+>>> linebreaksbr(123)
+'123'
+>>> removetags(123, 'a')
+'123'
+>>> striptags(123)
+'123'
 
-
 """
 
 from django.template.defaultfilters import *
